desktop improvements, no error on empty responses, no redirect in host

This commit is contained in:
JHubi1 2024-07-23 16:25:39 +02:00
parent fcb20e7931
commit edcbf74295
No known key found for this signature in database
GPG Key ID: 7BF82570CBBBD050
7 changed files with 806 additions and 408 deletions

View File

@ -50,6 +50,16 @@
"description": "Fifth tip displayed in the sidebar", "description": "Fifth tip displayed in the sidebar",
"context": "Visible 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": "Take Image",
"@takeImage": { "@takeImage": {
"description": "Text displayed for take image button", "description": "Text displayed for take image button",
@ -265,10 +275,10 @@
"description": "Text displayed when the host is invalid", "description": "Text displayed when the host is invalid",
"context": "Visible in the settings view", "context": "Visible in the settings view",
"placeholders": { "placeholders": {
"type": { "type": {
"type": "String", "type": "String",
"description": "Type of the issue, either 'url' or 'other' (preferably 'host')" "description": "Type of the issue, either 'url' or 'other' (preferably 'host')"
} }
} }
}, },
"settingsHostHeaderTitle": "Set host header", "settingsHostHeaderTitle": "Set host header",
@ -286,10 +296,10 @@
"description": "Text displayed when the host is invalid", "description": "Text displayed when the host is invalid",
"context": "Visible in the settings view", "context": "Visible in the settings view",
"placeholders": { "placeholders": {
"type": { "type": {
"type": "String", "type": "String",
"description": "Type of the issue, either 'url' or 'other' (preferably 'host')" "description": "Type of the issue, either 'url' or 'other' (preferably 'host')"
} }
} }
}, },
"settingsSystemMessage": "System message", "settingsSystemMessage": "System message",
@ -382,10 +392,10 @@
"description": "Text displayed as description for keep model loaded for set time toggle", "description": "Text displayed as description for keep model loaded for set time toggle",
"context": "Visible in the settings view", "context": "Visible in the settings view",
"placeholders": { "placeholders": {
"minutes": { "minutes": {
"type": "String", "type": "String",
"description": "Minutes the model should be kept loaded" "description": "Minutes the model should be kept loaded"
} }
} }
}, },
"settingsEnableHapticFeedback": "Enable haptic feedback", "settingsEnableHapticFeedback": "Enable haptic feedback",
@ -574,10 +584,10 @@
"description": "Text displayed when an update is available", "description": "Text displayed when an update is available",
"context": "Visible in the settings view", "context": "Visible in the settings view",
"placeholders": { "placeholders": {
"version": { "version": {
"type": "String", "type": "String",
"description": "Version number of the available update" "description": "Version number of the available update"
} }
} }
}, },
"settingsUpdateRateLimit": "Can't check, API rate limit exceeded", "settingsUpdateRateLimit": "Can't check, API rate limit exceeded",
@ -640,10 +650,10 @@
"description": "Text displayed as description for version", "description": "Text displayed as description for version",
"context": "Visible in the settings view", "context": "Visible in the settings view",
"placeholders": { "placeholders": {
"version": { "version": {
"type": "String", "type": "String",
"description": "Version number of the app" "description": "Version number of the app"
} }
} }
} }
} }

View File

@ -39,7 +39,8 @@ import 'package:url_launcher/url_launcher.dart';
// use host or not, if false dialog is shown // use host or not, if false dialog is shown
const useHost = false; 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"; const fixedHost = "http://example.com:11434";
// use model or not, if false selector is shown // use model or not, if false selector is shown
const useModel = false; const useModel = false;
@ -66,6 +67,7 @@ bool multimodal = false;
List<types.Message> messages = []; List<types.Message> messages = [];
String? chatUuid; String? chatUuid;
bool chatAllowed = true; bool chatAllowed = true;
String hoveredChat = "";
final user = types.User(id: const Uuid().v4()); final user = types.User(id: const Uuid().v4());
final assistant = types.User(id: const Uuid().v4()); final assistant = types.User(id: const Uuid().v4());
@ -283,14 +285,13 @@ class _MainAppState extends State<MainApp> {
), ),
const SizedBox(width: 16), const SizedBox(width: 16),
]))))), ]))))),
desktopLayoutNotRequired(context) (desktopLayoutNotRequired(context) ||
(!allowMultipleChats && !allowSettings))
? const SizedBox.shrink() ? const SizedBox.shrink()
: (!allowMultipleChats && !allowSettings) : Divider(
? const SizedBox.shrink() color: desktopLayout(context)
: Divider( ? Theme.of(context).colorScheme.onSurface.withAlpha(20)
color: desktopLayout(context) : null),
? Theme.of(context).colorScheme.onSurface.withAlpha(20)
: null),
(allowMultipleChats) (allowMultipleChats)
? (Padding( ? (Padding(
padding: padding, padding: padding,
@ -360,10 +361,13 @@ class _MainAppState extends State<MainApp> {
const SizedBox(width: 16), const SizedBox(width: 16),
]))))) ])))))
: const SizedBox.shrink(), : const SizedBox.shrink(),
Divider( (desktopLayoutNotRequired(context) &&
color: desktopLayout(context) (!allowMultipleChats && !allowSettings))
? Theme.of(context).colorScheme.onSurface.withAlpha(20) ? const SizedBox.shrink()
: null), : Divider(
color: desktopLayout(context)
? Theme.of(context).colorScheme.onSurface.withAlpha(20)
: null),
((prefs?.getStringList("chats") ?? []).isNotEmpty) ((prefs?.getStringList("chats") ?? []).isNotEmpty)
? const SizedBox.shrink() ? const SizedBox.shrink()
: (Padding( : (Padding(
@ -444,135 +448,466 @@ class _MainAppState extends State<MainApp> {
}), }),
]) ])
..addAll((prefs?.getStringList("chats") ?? []).map((item) { ..addAll((prefs?.getStringList("chats") ?? []).map((item) {
return Dismissible( var child = Padding(
key: Key(jsonDecode(item)["uuid"]), padding: padding,
direction: (chatAllowed) child: InkWell(
? DismissDirection.startToEnd customBorder: const RoundedRectangleBorder(
: DismissDirection.none, borderRadius: BorderRadius.all(Radius.circular(50))),
confirmDismiss: (direction) async { onTap: () {
bool returnValue = false; selectionHaptic();
if (!chatAllowed) return false; if (!desktopLayoutRequired(context)) {
Navigator.of(context).pop();
if (prefs!.getBool("askBeforeDeletion") ?? false) { }
await showDialog( if (!chatAllowed) return;
context: context, if (chatUuid == jsonDecode(item)["uuid"]) return;
builder: (context) { loadChat(jsonDecode(item)["uuid"], setState);
return StatefulBuilder(builder: (context, setLocalState) { chatUuid = jsonDecode(item)["uuid"];
return AlertDialog( },
title: Text(AppLocalizations.of(context)! onHover: (value) {
.deleteDialogTitle), setState(() {
content: Column( if (value) {
mainAxisSize: MainAxisSize.min, hoveredChat = jsonDecode(item)["uuid"];
children: [ } else {
Text(AppLocalizations.of(context)! hoveredChat = "";
.deleteDialogDescription), }
]), });
actions: [ },
TextButton( onLongPress: desktopFeature()
onPressed: () { ? null
selectionHaptic(); : () async {
Navigator.of(context).pop(); selectionHaptic();
returnValue = false; if (!chatAllowed) return;
}, if (!allowSettings) return;
child: Text(AppLocalizations.of(context)! String oldTitle = jsonDecode(item)["title"];
.deleteDialogCancel)), var newTitle = await prompt(context,
TextButton( title: AppLocalizations.of(context)!
onPressed: () { .dialogEnterNewTitle,
selectionHaptic(); value: oldTitle,
Navigator.of(context).pop(); uuid: jsonDecode(item)["uuid"]);
returnValue = true; var tmp = (prefs!.getStringList("chats") ?? []);
}, for (var i = 0; i < tmp.length; i++) {
child: Text(AppLocalizations.of(context)! if (jsonDecode((prefs!.getStringList("chats") ??
.deleteDialogDelete)) [])[i])["uuid"] ==
]); jsonDecode(item)["uuid"]) {
}); var tmp2 = jsonDecode(tmp[i]);
}); tmp2["title"] = newTitle;
} else { tmp[i] = jsonEncode(tmp2);
returnValue = true; break;
} }
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<String> 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;
} }
} prefs!.setStringList("chats", tmp);
prefs!.setStringList("chats", tmp); setState(() {});
setState(() {}); },
}, child: Padding(
child: Padding( padding: const EdgeInsets.only(top: 16, bottom: 16),
padding: const EdgeInsets.only(top: 16, bottom: 16), child: Row(children: [
child: Row(children: [ allowMultipleChats
Padding( ? Padding(
padding: padding:
const EdgeInsets.only(left: 16, right: 16), const EdgeInsets.only(left: 16, right: 16),
child: Icon((chatUuid == jsonDecode(item)["uuid"]) child: Icon((chatUuid == jsonDecode(item)["uuid"])
? Icons.location_on_rounded ? Icons.location_on_rounded
: Icons.restore_rounded)), : Icons.restore_rounded))
Expanded( : const SizedBox(width: 16),
child: Text(jsonDecode(item)["title"], Expanded(
softWrap: false, child: Text(jsonDecode(item)["title"],
maxLines: 1, softWrap: false,
overflow: TextOverflow.fade, maxLines: 1,
style: const TextStyle( overflow: TextOverflow.fade,
fontWeight: FontWeight.w500)), style:
), const TextStyle(fontWeight: FontWeight.w500)),
const SizedBox(width: 16), ),
]))))); 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<String> 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<String>
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<String> 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<String> 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<String> 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<String> 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()); }).toList());
} }
@ -610,7 +945,9 @@ class _MainAppState extends State<MainApp> {
padding: EdgeInsets.all(16), padding: EdgeInsets.all(16),
child: Text( 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.", "*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<MainApp> {
: [Expanded(child: selector)]), : [Expanded(child: selector)]),
actions: desktopControlsActions(context, [ actions: desktopControlsActions(context, [
const SizedBox(width: 4), const SizedBox(width: 4),
IconButton( allowMultipleChats
onPressed: () { ? IconButton(
selectionHaptic(); onPressed: () {
if (!chatAllowed) return; selectionHaptic();
if (!chatAllowed) return;
if (prefs!.getBool("askBeforeDeletion") ?? if (prefs!.getBool("askBeforeDeletion") ??
// ignore: dead_code // ignore: dead_code
false && messages.isNotEmpty) { false && messages.isNotEmpty) {
showDialog( showDialog(
context: context, context: context,
builder: (context) { builder: (context) {
return StatefulBuilder( return StatefulBuilder(
builder: (context, setLocalState) { builder: (context, setLocalState) {
return AlertDialog( return AlertDialog(
title: Text(AppLocalizations.of(context)! title: Text(
.deleteDialogTitle), AppLocalizations.of(context)!
content: Column( .deleteDialogTitle),
mainAxisSize: MainAxisSize.min, content: Column(
children: [ mainAxisSize: MainAxisSize.min,
Text(AppLocalizations.of(context)! children: [
.deleteDialogDescription), Text(AppLocalizations.of(context)!
]), .deleteDialogDescription),
actions: [ ]),
TextButton( actions: [
onPressed: () { TextButton(
selectionHaptic(); onPressed: () {
Navigator.of(context).pop(); selectionHaptic();
}, Navigator.of(context).pop();
child: Text( },
AppLocalizations.of(context)! child: Text(
.deleteDialogCancel)), AppLocalizations.of(context)!
TextButton( .deleteDialogCancel)),
onPressed: () { TextButton(
selectionHaptic(); onPressed: () {
Navigator.of(context).pop(); selectionHaptic();
Navigator.of(context).pop();
for (var i = 0; for (var i = 0;
i < i <
(prefs!.getStringList( (prefs!.getStringList(
"chats") ?? "chats") ??
[]) [])
.length; .length;
i++) { i++) {
if (jsonDecode((prefs! if (jsonDecode((prefs!
.getStringList( .getStringList(
"chats") ?? "chats") ??
[])[i])["uuid"] == [])[i])["uuid"] ==
chatUuid) { chatUuid) {
List<String> tmp = prefs! List<String> tmp = prefs!
.getStringList("chats")!; .getStringList(
tmp.removeAt(i); "chats")!;
prefs!.setStringList( tmp.removeAt(i);
"chats", tmp); prefs!.setStringList(
break; "chats", tmp);
} break;
} }
messages = []; }
chatUuid = null; messages = [];
setState(() {}); chatUuid = null;
}, setState(() {});
child: Text( },
AppLocalizations.of(context)! child: Text(
.deleteDialogDelete)) AppLocalizations.of(context)!
]); .deleteDialogDelete))
}); ]);
}); });
} else { });
for (var i = 0; } else {
i < (prefs!.getStringList("chats") ?? []).length; for (var i = 0;
i++) { i <
if (jsonDecode((prefs!.getStringList("chats") ?? (prefs!.getStringList("chats") ?? [])
[])[i])["uuid"] == .length;
chatUuid) { i++) {
List<String> tmp = prefs!.getStringList("chats")!; if (jsonDecode((prefs!.getStringList("chats") ??
tmp.removeAt(i); [])[i])["uuid"] ==
prefs!.setStringList("chats", tmp); chatUuid) {
break; List<String> tmp =
prefs!.getStringList("chats")!;
tmp.removeAt(i);
prefs!.setStringList("chats", tmp);
break;
}
}
messages = [];
chatUuid = null;
} }
} setState(() {});
messages = []; },
chatUuid = null; icon: const Icon(Icons.restart_alt_rounded))
} : const SizedBox.shrink()
setState(() {});
},
icon: const Icon(Icons.restart_alt_rounded))
]), ]),
bottom: PreferredSize( bottom: PreferredSize(
preferredSize: const Size.fromHeight(1), preferredSize: const Size.fromHeight(1),
@ -875,6 +1219,13 @@ class _MainAppState extends State<MainApp> {
{required messageWidth, required showName}) { {required messageWidth, required showName}) {
var white = var white =
const TextStyle(color: Colors.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( return Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 20, left: 20,
@ -889,7 +1240,7 @@ class _MainAppState extends State<MainApp> {
WidgetStatePropertyAll( WidgetStatePropertyAll(
Colors.grey))), Colors.grey))),
child: MarkdownBody( child: MarkdownBody(
data: p0.text, data: text,
onTapLink: (text, href, title) async { onTapLink: (text, href, title) async {
selectionHaptic(); selectionHaptic();
try { try {
@ -1020,9 +1371,8 @@ class _MainAppState extends State<MainApp> {
Colors.white), Colors.white),
codeblockDecoration: BoxDecoration( codeblockDecoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: borderRadius: BorderRadius.circular(
BorderRadius.circular( 8)),
8)),
h1: white, h1: white,
h2: white, h2: white,
h3: white, h3: white,
@ -1042,8 +1392,10 @@ class _MainAppState extends State<MainApp> {
: (Theme.of(context).brightness == : (Theme.of(context).brightness ==
Brightness.light) Brightness.light)
? MarkdownStyleSheet( ? MarkdownStyleSheet(
p: const TextStyle( p: TextStyle(
color: Colors.black, color: greyed
? Colors.grey
: Colors.black,
fontSize: 16, fontSize: 16,
fontWeight: fontWeight:
FontWeight.w500), FontWeight.w500),
@ -1056,8 +1408,7 @@ class _MainAppState extends State<MainApp> {
), ),
code: const TextStyle( code: const TextStyle(
color: Colors.white, color: Colors.white,
backgroundColor: backgroundColor: Colors.black),
Colors.black),
codeblockDecoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(8)), codeblockDecoration: BoxDecoration(color: Colors.black, borderRadius: BorderRadius.circular(8)),
horizontalRuleDecoration: BoxDecoration(border: Border(top: BorderSide(color: Colors.grey[200]!, width: 1)))) horizontalRuleDecoration: BoxDecoration(border: Border(top: BorderSide(color: Colors.grey[200]!, width: 1))))
: MarkdownStyleSheet( : MarkdownStyleSheet(
@ -1476,7 +1827,7 @@ class _MainAppState extends State<MainApp> {
inputTextColor: (theme ?? ThemeData()).colorScheme.onSurface, inputTextColor: (theme ?? ThemeData()).colorScheme.onSurface,
inputBorderRadius: const BorderRadius.all(Radius.circular(64)), inputBorderRadius: const BorderRadius.all(Radius.circular(64)),
inputPadding: const EdgeInsets.all(16), 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) messageMaxWidth: (MediaQuery.of(context).size.width >= 1000)
? (MediaQuery.of(context).size.width >= 1600) ? (MediaQuery.of(context).size.width >= 1600)
? (MediaQuery.of(context).size.width >= 2200) ? (MediaQuery.of(context).size.width >= 2200)
@ -1532,7 +1883,7 @@ class _MainAppState extends State<MainApp> {
), ),
], ],
), ),
drawerEdgeDragWidth: (prefs!.getBool("fixCodeblockScroll") ?? false) drawerEdgeDragWidth: (prefs?.getBool("fixCodeblockScroll") ?? false)
? null ? null
: (desktopLayout(context) : (desktopLayout(context)
? null ? null

View File

@ -255,17 +255,21 @@ class _ScreenSettingsState extends State<ScreenSettings> {
return; return;
} }
http.Response request; http.Response? request;
try { try {
request = await http var client = http.Client();
.get( final requestBase = http.Request("get", Uri.parse(tmpHost))
Uri.parse(tmpHost), ..headers.addAll(
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map) (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
.cast<String, String>(), .cast<String, String>(),
) )
..followRedirects = false;
request = await http.Response.fromStream(await requestBase
.send()
.timeout(const Duration(seconds: 5), onTimeout: () { .timeout(const Duration(seconds: 5), onTimeout: () {
return http.Response("Error", 408); return http.StreamedResponse(const Stream.empty(), 408);
}); }));
client.close();
} catch (e) { } catch (e) {
setState(() { setState(() {
hostInvalidHost = true; hostInvalidHost = true;
@ -278,7 +282,8 @@ class _ScreenSettingsState extends State<ScreenSettings> {
setState(() { setState(() {
hostLoading = false; hostLoading = false;
host = tmpHost; host = tmpHost;
if (hostInputController.text != host!) { if (hostInputController.text != host! &&
(Uri.parse(tmpHost).toString() != fixedHost)) {
hostInputController.text = host!; hostInputController.text = host!;
} }
}); });

View File

@ -28,13 +28,11 @@ class _ScreenSettingsExportState extends State<ScreenSettingsExport> {
color: Theme.of(context).colorScheme.surface, color: Theme.of(context).colorScheme.surface,
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Row(children: [ title: Row(children: [
Text(AppLocalizations.of(context)!.settingsTitleExport), Text(AppLocalizations.of(context)!.settingsTitleExport),
Expanded(child: SizedBox(height: 200, child: MoveWindow())) Expanded(child: SizedBox(height: 200, child: MoveWindow()))
]), ]),
actions: actions: desktopControlsActions(context)),
desktopControlsActions(context)
),
body: Padding( body: Padding(
padding: const EdgeInsets.only(left: 16, right: 16), padding: const EdgeInsets.only(left: 16, right: 16),
child: Column(children: [ child: Column(children: [
@ -58,78 +56,89 @@ class _ScreenSettingsExportState extends State<ScreenSettingsExport> {
jsonEncode(prefs!.getStringList("chats") ?? [])); jsonEncode(prefs!.getStringList("chats") ?? []));
} }
}), }),
button(AppLocalizations.of(context)!.settingsImportChats, allowMultipleChats
Icons.download_rounded, () { ? button(
selectionHaptic(); AppLocalizations.of(context)!.settingsImportChats,
showDialog( Icons.download_rounded, () {
context: context, selectionHaptic();
builder: (context) { showDialog(
return AlertDialog( context: context,
title: Text(AppLocalizations.of(context)! builder: (context) {
.settingsImportChatsTitle), return AlertDialog(
content: Text(AppLocalizations.of(context)! title: Text(AppLocalizations.of(context)!
.settingsImportChatsDescription), .settingsImportChatsTitle),
actions: [ content: Text(
TextButton( AppLocalizations.of(context)!
onPressed: () { .settingsImportChatsDescription),
selectionHaptic(); actions: [
Navigator.of(context).pop(); TextButton(
}, onPressed: () {
child: Text(AppLocalizations.of(context)! selectionHaptic();
.settingsImportChatsCancel)), Navigator.of(context).pop();
TextButton( },
onPressed: () async { child: Text(AppLocalizations.of(
selectionHaptic(); context)!
FilePickerResult? result = .settingsImportChatsCancel)),
await FilePicker.platform.pickFiles( TextButton(
type: FileType.custom, onPressed: () async {
allowedExtensions: ["json"]); selectionHaptic();
if (result == null) { FilePickerResult? result =
// ignore: use_build_context_synchronously await FilePicker.platform
Navigator.of(context).pop(); .pickFiles(
return; type: FileType.custom,
} allowedExtensions: [
"json"
]);
if (result == null) {
// ignore: use_build_context_synchronously
Navigator.of(context).pop();
return;
}
File file = File file = File(
File(result.files.single.path!); result.files.single.path!);
var content = await file.readAsString(); var content =
List<dynamic> tmpHistory = await file.readAsString();
jsonDecode(content); List<dynamic> tmpHistory =
List<String> history = []; jsonDecode(content);
List<String> history = [];
for (var i = 0; for (var i = 0;
i < tmpHistory.length; i < tmpHistory.length;
i++) { i++) {
history.add(tmpHistory[i]); history.add(tmpHistory[i]);
} }
prefs!.setStringList("chats", history); prefs!.setStringList(
"chats", history);
messages = []; messages = [];
chatUuid = null; chatUuid = null;
setState(() {}); setState(() {});
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
Navigator.of(context).pop(); Navigator.of(context).pop();
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
Navigator.of(context).pop(); Navigator.of(context).pop();
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
Navigator.of(context).pop(); Navigator.of(context).pop();
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
ScaffoldMessenger.of(context) ScaffoldMessenger.of(context)
.showSnackBar(SnackBar( .showSnackBar(SnackBar(
content: Text(AppLocalizations content: Text(AppLocalizations
// ignore: use_build_context_synchronously // ignore: use_build_context_synchronously
.of(context)! .of(context)!
.settingsImportChatsSuccess), .settingsImportChatsSuccess),
showCloseIcon: true)); showCloseIcon: true));
}, },
child: Text(AppLocalizations.of(context)! child: Text(
.settingsImportChatsImport)) AppLocalizations.of(context)!
]); .settingsImportChatsImport))
}); ]);
}) });
})
: const SizedBox.shrink()
]), ]),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),

View File

@ -162,90 +162,90 @@ Future<String> send(String value, BuildContext context, Function setState,
.cast<String, String>(), .cast<String, String>(),
baseUrl: "$host/api"); baseUrl: "$host/api");
try { // try {
if ((prefs!.getString("requestType") ?? "stream") == "stream") { if ((prefs!.getString("requestType") ?? "stream") == "stream") {
final stream = client final stream = client
.generateChatCompletionStream( .generateChatCompletionStream(
request: llama.GenerateChatCompletionRequest( request: llama.GenerateChatCompletionRequest(
model: model!, model: model!,
messages: history, messages: history,
keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")), keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
) )
.timeout(const Duration(seconds: 30)); .timeout(const Duration(seconds: 30));
await for (final res in stream) { await for (final res in stream) {
text += (res.message?.content ?? ""); text += (res.message?.content ?? "");
for (var i = 0; i < messages.length; i++) { for (var i = 0; i < messages.length; i++) {
if (messages[i].id == newId) { if (messages[i].id == newId) {
messages.removeAt(i); messages.removeAt(i);
break; 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 (chatAllowed) return "";
if (request.message!.content.trim() == "") { // if (text.trim() == "") {
throw Exception(); // throw Exception();
} // }
messages.insert( messages.insert(
0, 0, types.TextMessage(author: assistant, id: newId, text: text));
types.TextMessage( if (onStream != null) {
author: assistant, id: newId, text: request.message!.content)); onStream(text, false);
text = request.message!.content; }
setState(() {}); setState(() {});
heavyHaptic(); heavyHaptic();
} }
} catch (e) { } else {
for (var i = 0; i < messages.length; i++) { llama.GenerateChatCompletionResponse request;
if (messages[i].id == newId) { request = await client
messages.removeAt(i); .generateChatCompletion(
break; request: llama.GenerateChatCompletionRequest(
} model: model!,
} messages: history,
setState(() { keepAlive: int.parse(prefs!.getString("keepAlive") ?? "300")),
chatAllowed = true; )
messages.removeAt(0); .timeout(const Duration(seconds: 30));
if (messages.isEmpty) { if (chatAllowed) return "";
var tmp = (prefs!.getStringList("chats") ?? []); // if (request.message!.content.trim() == "") {
chatUuid = null; // throw Exception();
for (var i = 0; i < tmp.length; i++) { // }
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == messages.insert(
chatUuid) { 0,
tmp.removeAt(i); types.TextMessage(
prefs!.setStringList("chats", tmp); author: assistant, id: newId, text: request.message!.content));
break; text = request.message!.content;
} setState(() {});
} heavyHaptic();
}
});
// 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 "";
} }
// } 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 ((prefs!.getString("requestType") ?? "stream") == "stream") {
if (onStream != null) { if (onStream != null) {

View File

@ -82,7 +82,8 @@ void setModel(BuildContext context, Function setState) {
if (!loaded) return; if (!loaded) return;
loaded = false; loaded = false;
if (usedIndex >= 0 && modelsReal[usedIndex] != model) { if (usedIndex >= 0 && modelsReal[usedIndex] != model) {
if (prefs!.getBool("resetOnModelSelect") ?? true) { if (prefs!.getBool("resetOnModelSelect") ??
true && allowMultipleChats) {
messages = []; messages = [];
chatUuid = null; chatUuid = null;
} }
@ -467,6 +468,24 @@ Future<String> prompt(BuildContext context,
autofocus: true, autofocus: true,
keyboardType: keyboard, keyboardType: keyboard,
maxLines: maxLines, 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( decoration: InputDecoration(
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
hintText: placeholder, hintText: placeholder,

View File

@ -1,5 +1,7 @@
{ {
"de": [ "de": [
"deleteChat",
"renameChat",
"settingsDescriptionBehavior", "settingsDescriptionBehavior",
"settingsDescriptionInterface", "settingsDescriptionInterface",
"settingsDescriptionVoice", "settingsDescriptionVoice",
@ -14,6 +16,8 @@
], ],
"zh": [ "zh": [
"deleteChat",
"renameChat",
"settingsDescriptionBehavior", "settingsDescriptionBehavior",
"settingsDescriptionInterface", "settingsDescriptionInterface",
"settingsDescriptionVoice", "settingsDescriptionVoice",