903 lines
36 KiB
Dart
903 lines
36 KiB
Dart
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
|
|
import 'desktop.dart';
|
|
import 'haptic.dart';
|
|
import '../main.dart';
|
|
import 'sender.dart';
|
|
import 'theme.dart';
|
|
import 'clients.dart';
|
|
|
|
import 'package:ollama_app/l10n/gen/app_localizations.dart';
|
|
|
|
import 'package:dartx/dartx.dart';
|
|
import 'package:ollama_dart/ollama_dart.dart' as llama;
|
|
// ignore: depend_on_referenced_packages
|
|
import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
|
import 'package:uuid/uuid.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
void setModel(BuildContext context, Function setState) {
|
|
List<String> models = [];
|
|
List<String> modelsReal = [];
|
|
List<bool> modal = [];
|
|
int usedIndex = -1;
|
|
int oldIndex = -1;
|
|
int addIndex = -1;
|
|
bool loaded = false;
|
|
Function? setModalState;
|
|
desktopTitleVisible = false;
|
|
setState(() {});
|
|
void load() async {
|
|
try {
|
|
var list = await ollamaClient.listModels().timeout(Duration(
|
|
seconds:
|
|
(10.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0)).round()));
|
|
for (var i = 0; i < list.models!.length; i++) {
|
|
var details = await ollamaClient.showModelInfo(
|
|
request: llama.ModelInfoRequest(model: list.models![i].model!));
|
|
models.add(list.models![i].model!.split(":")[0]);
|
|
modelsReal.add(list.models![i].model!);
|
|
modal.add((list.models![i].details!.families ?? []).contains("clip") ||
|
|
(details.capabilities ?? []).contains(llama.Capability.vision));
|
|
}
|
|
|
|
addIndex = models.length;
|
|
// ignore: use_build_context_synchronously
|
|
models.add(AppLocalizations.of(context)!.modelDialogAddModel);
|
|
// ignore: use_build_context_synchronously
|
|
modelsReal.add(AppLocalizations.of(context)!.modelDialogAddModel);
|
|
modal.add(false);
|
|
|
|
for (var i = 0; i < modelsReal.length; i++) {
|
|
if (modelsReal[i] == model) {
|
|
usedIndex = i;
|
|
oldIndex = usedIndex;
|
|
}
|
|
}
|
|
if (prefs!.getBool("modelTags") == null) {
|
|
List duplicateFinder = [];
|
|
for (var model in models) {
|
|
if (duplicateFinder.contains(model)) {
|
|
prefs!.setBool("modelTags", true);
|
|
break;
|
|
} else {
|
|
duplicateFinder.add(model);
|
|
}
|
|
}
|
|
}
|
|
loaded = true;
|
|
setModalState!(() {});
|
|
} catch (_) {
|
|
setState(() {
|
|
desktopTitleVisible = true;
|
|
});
|
|
// ignore: use_build_context_synchronously
|
|
Navigator.of(context).pop();
|
|
// ignore: use_build_context_synchronously
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
content: Text(
|
|
// ignore: use_build_context_synchronously
|
|
AppLocalizations.of(context)!.settingsHostInvalid("timeout")),
|
|
showCloseIcon: true));
|
|
}
|
|
}
|
|
|
|
if (useModel) return;
|
|
selectionHaptic();
|
|
|
|
load();
|
|
|
|
var content = StatefulBuilder(builder: (context, setLocalState) {
|
|
setModalState = setLocalState;
|
|
return PopScope(
|
|
canPop: false,
|
|
onPopInvokedWithResult: (didPop, result) async {
|
|
if (!loaded) return;
|
|
loaded = false;
|
|
bool preload = false;
|
|
if (usedIndex >= 0 && modelsReal[usedIndex] != model) {
|
|
preload = true;
|
|
if (prefs!.getBool("resetOnModelSelect") ??
|
|
true && allowMultipleChats) {
|
|
messages = [];
|
|
chatUuid = null;
|
|
}
|
|
}
|
|
model = (usedIndex >= 0) ? modelsReal[usedIndex] : null;
|
|
chatAllowed = !(model == null);
|
|
multimodal = (usedIndex >= 0) ? modal[usedIndex] : false;
|
|
if (model != null) {
|
|
prefs?.setString("model", model!);
|
|
} else {
|
|
prefs?.remove("model");
|
|
}
|
|
prefs?.setBool("multimodal", multimodal);
|
|
|
|
if (model != null &&
|
|
preload &&
|
|
int.parse(prefs!.getString("keepAlive") ?? "300") != 0 &&
|
|
(prefs!.getBool("preloadModel") ?? true)) {
|
|
setLocalState(() {});
|
|
try {
|
|
// don't use llama client, package doesn't support just loading without content
|
|
await httpClient
|
|
.post(
|
|
Uri.parse("$host/api/generate"),
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
...(jsonDecode(prefs!.getString("hostHeaders") ?? "{}")
|
|
as Map)
|
|
}.cast<String, String>(),
|
|
body: jsonEncode({
|
|
"model": model!,
|
|
"keep_alive":
|
|
int.parse(prefs!.getString("keepAlive") ?? "300")
|
|
}),
|
|
)
|
|
.timeout(Duration(
|
|
seconds: (10.0 *
|
|
(prefs!.getDouble("timeoutMultiplier") ?? 1.0))
|
|
.round()));
|
|
} catch (_) {
|
|
// ignore: use_build_context_synchronously
|
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
|
// ignore: use_build_context_synchronously
|
|
content: Text(AppLocalizations.of(context)!
|
|
.settingsHostInvalid("timeout")),
|
|
showCloseIcon: true));
|
|
setState(() {
|
|
model = null;
|
|
chatAllowed = false;
|
|
});
|
|
}
|
|
setState(() {
|
|
desktopTitleVisible = true;
|
|
});
|
|
// ignore: use_build_context_synchronously
|
|
Navigator.of(context).pop();
|
|
} else {
|
|
setState(() {
|
|
desktopTitleVisible = true;
|
|
});
|
|
try {
|
|
Navigator.of(context).pop();
|
|
} catch (_) {}
|
|
}
|
|
},
|
|
child: Container(
|
|
width: desktopLayout(context) ? null : double.infinity,
|
|
padding: EdgeInsets.only(
|
|
left: 16,
|
|
right: 16,
|
|
top: 16,
|
|
bottom: desktopLayout(context) ? 16 : 0),
|
|
child: (!loaded)
|
|
? SizedBox(
|
|
width: desktopLayout(context) ? 300 : double.infinity,
|
|
child: const LinearProgressIndicator())
|
|
: Column(mainAxisSize: MainAxisSize.min, children: [
|
|
Container(
|
|
width: desktopLayout(context) ? 300 : double.infinity,
|
|
constraints: BoxConstraints(
|
|
maxHeight:
|
|
MediaQuery.of(context).size.height * 0.4),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.vertical,
|
|
child: Wrap(
|
|
spacing: desktopLayout(context) ? 10.0 : 5.0,
|
|
runSpacing:
|
|
desktopFeature(web: true) ? 10.0 : 0.0,
|
|
alignment: WrapAlignment.center,
|
|
children: List<Widget>.generate(
|
|
models.length,
|
|
(int index) {
|
|
return ChoiceChip(
|
|
label: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(models[index]),
|
|
((prefs!.getBool("modelTags") ??
|
|
false) &&
|
|
modelsReal[index]
|
|
.split(":")
|
|
.length >
|
|
1)
|
|
? Text(
|
|
":${modelsReal[index].split(":")[1]}",
|
|
style: const TextStyle(
|
|
color: Colors.grey))
|
|
: const SizedBox.shrink()
|
|
]),
|
|
selected: usedIndex == index,
|
|
avatar: (usedIndex == index)
|
|
? null
|
|
: (addIndex == index)
|
|
? const Icon(Icons.add_rounded)
|
|
: ((recommendedModels
|
|
.contains(models[index]))
|
|
? const Icon(Icons.star_rounded)
|
|
: ((modal[index])
|
|
? const Icon(Icons
|
|
.collections_rounded)
|
|
: null)),
|
|
checkmarkColor: (usedIndex == index &&
|
|
!(prefs?.getBool(
|
|
"useDeviceTheme") ??
|
|
false))
|
|
? ((MediaQuery.of(context)
|
|
.platformBrightness ==
|
|
Brightness.light)
|
|
? themeLight().colorScheme.secondary
|
|
: themeDark().colorScheme.secondary)
|
|
: null,
|
|
labelStyle: (usedIndex == index &&
|
|
!(prefs?.getBool(
|
|
"useDeviceTheme") ??
|
|
false))
|
|
? TextStyle(
|
|
color: (MediaQuery.of(context)
|
|
.platformBrightness ==
|
|
Brightness.light)
|
|
? themeLight()
|
|
.colorScheme
|
|
.secondary
|
|
: themeDark()
|
|
.colorScheme
|
|
.secondary)
|
|
: null,
|
|
selectedColor: (prefs
|
|
?.getBool("useDeviceTheme") ??
|
|
false)
|
|
? null
|
|
: (MediaQuery.of(context)
|
|
.platformBrightness ==
|
|
Brightness.light)
|
|
? themeLight().colorScheme.primary
|
|
: themeDark().colorScheme.primary,
|
|
onSelected: (bool selected) {
|
|
selectionHaptic();
|
|
if (addIndex == index) {
|
|
usedIndex = oldIndex;
|
|
Navigator.of(context).pop();
|
|
addModel(context, setState);
|
|
}
|
|
if (!chatAllowed && model != null) {
|
|
return;
|
|
}
|
|
setLocalState(() {
|
|
usedIndex = selected ? index : -1;
|
|
});
|
|
},
|
|
);
|
|
},
|
|
).toList(),
|
|
)))
|
|
])));
|
|
});
|
|
|
|
if (desktopLayoutNotRequired(context)) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (context) {
|
|
return Transform.translate(
|
|
offset: desktopLayoutRequired(context)
|
|
? const Offset(289, 0)
|
|
: const Offset(0, 0),
|
|
child: Dialog(
|
|
surfaceTintColor:
|
|
(Theme.of(context).brightness == Brightness.dark)
|
|
? Colors.grey[800]
|
|
: null,
|
|
alignment: desktopLayoutRequired(context)
|
|
? Alignment.topLeft
|
|
: Alignment.topCenter,
|
|
child: content),
|
|
);
|
|
});
|
|
} else {
|
|
showModalBottomSheet(
|
|
context: context, builder: (context) => Container(child: content));
|
|
}
|
|
}
|
|
|
|
void addModel(BuildContext context, Function setState) async {
|
|
bool canceled = false;
|
|
bool networkError = false;
|
|
bool ratelimitError = false;
|
|
bool alreadyExists = false;
|
|
final String invalidText =
|
|
AppLocalizations.of(context)!.modelDialogAddPromptInvalid;
|
|
final networkErrorText =
|
|
AppLocalizations.of(context)!.settingsHostInvalid("other");
|
|
final timeoutErrorText =
|
|
AppLocalizations.of(context)!.settingsHostInvalid("timeout");
|
|
final ratelimitErrorText =
|
|
AppLocalizations.of(context)!.settingsHostInvalid("ratelimit");
|
|
final alreadyExistsText =
|
|
AppLocalizations.of(context)!.modelDialogAddPromptAlreadyExists;
|
|
final downloadSuccessText =
|
|
AppLocalizations.of(context)!.modelDialogAddDownloadSuccess;
|
|
final downloadFailedText =
|
|
AppLocalizations.of(context)!.modelDialogAddDownloadFailed;
|
|
var requestedModel = await prompt(
|
|
context,
|
|
title: AppLocalizations.of(context)!.modelDialogAddPromptTitle,
|
|
description: AppLocalizations.of(context)!.modelDialogAddPromptDescription,
|
|
placeholder: "llama3:latest",
|
|
enableSuggestions: false,
|
|
validator: (content) async {
|
|
var model = content.trim();
|
|
model = model.removeSuffix(":latest");
|
|
if (model == "") return false;
|
|
canceled = false;
|
|
networkError = false;
|
|
ratelimitError = false;
|
|
alreadyExists = false;
|
|
try {
|
|
var request = await ollamaClient.listModels().timeout(Duration(
|
|
seconds: (10.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0))
|
|
.round()));
|
|
for (var element in request.models!) {
|
|
var localModel = element.model!.removeSuffix(":latest");
|
|
if (localModel == model) {
|
|
alreadyExists = true;
|
|
}
|
|
}
|
|
if (alreadyExists) return false;
|
|
} catch (_) {
|
|
networkError = true;
|
|
return false;
|
|
}
|
|
var endpoint = "https://ollama.com/library/";
|
|
if (kIsWeb) {
|
|
if (!(prefs!.getBool("allowWebProxy") ?? false)) {
|
|
bool returnValue = false;
|
|
await showDialog(
|
|
context: mainContext!,
|
|
barrierDismissible: false,
|
|
builder: (context) {
|
|
return AlertDialog(
|
|
title: Text(AppLocalizations.of(context)!
|
|
.modelDialogAddAllowanceTitle),
|
|
content: SizedBox(
|
|
width: 640,
|
|
child: Text(AppLocalizations.of(context)!
|
|
.modelDialogAddAllowanceDescription),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
canceled = true;
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: Text(AppLocalizations.of(context)!
|
|
.modelDialogAddAllowanceDeny)),
|
|
TextButton(
|
|
onPressed: () {
|
|
returnValue = true;
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: Text(AppLocalizations.of(context)!
|
|
.modelDialogAddAllowanceAllow))
|
|
]);
|
|
});
|
|
if (!returnValue) return false;
|
|
prefs!.setBool("allowWebProxy", true);
|
|
}
|
|
endpoint = "https://end.jhubi1.com/ollama-proxy/";
|
|
}
|
|
http.Response response;
|
|
try {
|
|
response = await httpClient
|
|
.get(Uri.parse("$endpoint${Uri.encodeComponent(model)}"))
|
|
.timeout(Duration(
|
|
seconds: (10.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0))
|
|
.round()));
|
|
} catch (_) {
|
|
networkError = true;
|
|
return false;
|
|
}
|
|
if (response.statusCode == 200) {
|
|
bool returnValue = false;
|
|
await showDialog(
|
|
context: mainContext!,
|
|
barrierDismissible: false,
|
|
builder: (context) {
|
|
return AlertDialog(
|
|
title: Text(AppLocalizations.of(context)!
|
|
.modelDialogAddAssuranceTitle(model)),
|
|
content: SizedBox(
|
|
width: 640,
|
|
child: Text(AppLocalizations.of(context)!
|
|
.modelDialogAddAssuranceDescription(model)),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () {
|
|
canceled = true;
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: Text(AppLocalizations.of(context)!
|
|
.modelDialogAddAssuranceCancel)),
|
|
TextButton(
|
|
onPressed: () {
|
|
returnValue = true;
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: Text(AppLocalizations.of(context)!
|
|
.modelDialogAddAssuranceAdd))
|
|
]);
|
|
});
|
|
return returnValue;
|
|
}
|
|
if (response.statusCode == 429) {
|
|
ratelimitError = true;
|
|
}
|
|
return false;
|
|
},
|
|
validatorErrorCallback: (content) {
|
|
if (networkError) return networkErrorText;
|
|
if (ratelimitError) return ratelimitErrorText;
|
|
if (alreadyExists) return alreadyExistsText;
|
|
if (canceled) return null;
|
|
return invalidText;
|
|
},
|
|
);
|
|
if (requestedModel == "") return;
|
|
requestedModel = requestedModel.removeSuffix(":latest");
|
|
double? percent;
|
|
Function? setDialogState;
|
|
showModalBottomSheet(
|
|
context: mainContext!,
|
|
builder: (context) {
|
|
return StatefulBuilder(builder: (context, setLocalState) {
|
|
setDialogState = setLocalState;
|
|
return PopScope(
|
|
canPop: false,
|
|
child: Container(
|
|
width: double.infinity,
|
|
padding: EdgeInsets.only(
|
|
left: 16,
|
|
right: 16,
|
|
top: 16,
|
|
bottom: desktopLayout(context) ? 16 : 0),
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Text(
|
|
percent == null
|
|
? AppLocalizations.of(context)!
|
|
.modelDialogAddDownloadPercentLoading
|
|
: AppLocalizations.of(context)!
|
|
.modelDialogAddDownloadPercent(
|
|
(percent * 100).round().toString()),
|
|
),
|
|
const Padding(padding: EdgeInsets.only(top: 8)),
|
|
LinearProgressIndicator(value: percent),
|
|
],
|
|
)));
|
|
});
|
|
});
|
|
try {
|
|
final stream = ollamaClient
|
|
.pullModelStream(request: llama.PullModelRequest(model: requestedModel))
|
|
.timeout(Duration(
|
|
seconds: (10.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0))
|
|
.round()));
|
|
bool alreadyProgressed = false;
|
|
await for (final res in stream) {
|
|
double tmpPercent =
|
|
((res.completed ?? 0).toInt() / (res.total ?? 100).toInt());
|
|
if ((tmpPercent * 100).round() == 0) {
|
|
if (!alreadyProgressed) {
|
|
percent = null;
|
|
}
|
|
} else {
|
|
percent = tmpPercent;
|
|
alreadyProgressed = true;
|
|
}
|
|
setDialogState!(() {});
|
|
}
|
|
// done downloading
|
|
if (prefs!.getBool("resetOnModelSelect") ?? true && allowMultipleChats) {
|
|
messages = [];
|
|
chatUuid = null;
|
|
}
|
|
model = requestedModel;
|
|
if (model!.split(":").length == 1) {
|
|
model = "$model:latest";
|
|
}
|
|
bool exists = false;
|
|
try {
|
|
var request = await ollamaClient.listModels().timeout(Duration(
|
|
seconds:
|
|
(10.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0)).round()));
|
|
for (var element in request.models!) {
|
|
if (element.model == model) {
|
|
exists = true;
|
|
multimodal = (element.details!.families ?? []).contains("clip");
|
|
}
|
|
}
|
|
if (!exists) {
|
|
throw Exception();
|
|
}
|
|
} catch (_) {
|
|
setState(() {
|
|
model = null;
|
|
multimodal = false;
|
|
chatAllowed = false;
|
|
});
|
|
prefs?.remove("model");
|
|
prefs?.setBool("multimodal", multimodal);
|
|
Navigator.of(mainContext!).pop();
|
|
if (!exists) {
|
|
ScaffoldMessenger.of(mainContext!).showSnackBar(
|
|
SnackBar(content: Text(downloadFailedText), showCloseIcon: true));
|
|
} else {
|
|
ScaffoldMessenger.of(mainContext!).showSnackBar(
|
|
SnackBar(content: Text(timeoutErrorText), showCloseIcon: true));
|
|
}
|
|
return;
|
|
}
|
|
prefs?.setString("model", model!);
|
|
prefs?.setBool("multimodal", multimodal);
|
|
setState(() {
|
|
chatAllowed = true;
|
|
});
|
|
Navigator.of(mainContext!).pop();
|
|
ScaffoldMessenger.of(mainContext!).showSnackBar(
|
|
SnackBar(content: Text(downloadSuccessText), showCloseIcon: true));
|
|
} catch (_) {
|
|
Navigator.of(mainContext!).pop();
|
|
ScaffoldMessenger.of(mainContext!).showSnackBar(
|
|
SnackBar(content: Text(downloadFailedText), showCloseIcon: true));
|
|
}
|
|
}
|
|
|
|
void saveChat(String uuid, Function setState) async {
|
|
int index = -1;
|
|
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
|
|
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == uuid) {
|
|
index = i;
|
|
}
|
|
}
|
|
if (index == -1) return;
|
|
List<Map<String, String>> history = [];
|
|
for (var i = 0; i < messages.length; i++) {
|
|
if ((jsonDecode(jsonEncode(messages[i])) as Map).containsKey("text")) {
|
|
history.add({
|
|
"role": (messages[i].author == user) ? "user" : "assistant",
|
|
"content": jsonDecode(jsonEncode(messages[i]))["text"]
|
|
});
|
|
} else {
|
|
var uri = jsonDecode(jsonEncode(messages[i]))["uri"] as String;
|
|
String content = (uri.startsWith("data:image/png;base64,"))
|
|
? uri.removePrefix("data:image/png;base64,")
|
|
: base64.encode(await File(uri).readAsBytes());
|
|
history.add({
|
|
"role": (messages[i].author == user) ? "user" : "assistant",
|
|
"type": "image",
|
|
"name": (messages[i] as types.ImageMessage).name,
|
|
"size": (messages[i] as types.ImageMessage).size.toString(),
|
|
"content": content
|
|
});
|
|
}
|
|
}
|
|
if (messages.isEmpty && uuid == chatUuid) {
|
|
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
|
|
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
|
|
chatUuid) {
|
|
List<String> tmp = prefs!.getStringList("chats")!;
|
|
tmp.removeAt(i);
|
|
prefs!.setStringList("chats", tmp);
|
|
chatUuid = null;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (jsonDecode((prefs!.getStringList("chats") ?? [])[index])["messages"]
|
|
.length >=
|
|
1) {
|
|
if (jsonDecode(jsonDecode((prefs!.getStringList("chats") ?? [])[index])[
|
|
"messages"])[0]["role"] ==
|
|
"system") {
|
|
history.add({
|
|
"role": "system",
|
|
"content": jsonDecode(jsonDecode(
|
|
(prefs!.getStringList("chats") ?? [])[index])["messages"])[0]
|
|
["content"]
|
|
});
|
|
}
|
|
} else {
|
|
var system = prefs?.getString("system") ?? "You are a helpful assistant";
|
|
if (prefs!.getBool("noMarkdown") ?? false) {
|
|
system +=
|
|
" You must not use markdown or any other formatting language in any way!";
|
|
}
|
|
history.add({"role": "system", "content": system});
|
|
}
|
|
history = history.reversed.toList();
|
|
List<String> tmp = prefs!.getStringList("chats") ?? [];
|
|
tmp.removeAt(index);
|
|
tmp.insert(
|
|
0,
|
|
jsonEncode({
|
|
"title":
|
|
jsonDecode((prefs!.getStringList("chats") ?? [])[index])["title"],
|
|
"uuid": uuid,
|
|
"model": model,
|
|
"messages": jsonEncode(history)
|
|
}));
|
|
prefs!.setStringList("chats", tmp);
|
|
setState(() {});
|
|
}
|
|
|
|
void loadChat(String uuid, Function setState) {
|
|
int index = -1;
|
|
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
|
|
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] == uuid) {
|
|
index = i;
|
|
}
|
|
}
|
|
if (index == -1) return;
|
|
messages = [];
|
|
model = null;
|
|
setState(() {});
|
|
var history = jsonDecode(
|
|
jsonDecode((prefs!.getStringList("chats") ?? [])[index])["messages"]);
|
|
for (var i = 0; i < history.length; i++) {
|
|
if (history[i]["role"] != "system") {
|
|
if ((history[i] as Map).containsKey("type") &&
|
|
history[i]["type"] == "image") {
|
|
messages.insert(
|
|
0,
|
|
types.ImageMessage(
|
|
author: (history[i]["role"] == "user") ? user : assistant,
|
|
id: const Uuid().v4(),
|
|
name: history[i]["name"],
|
|
size: int.parse(history[i]["size"]),
|
|
uri: "data:image/png;base64,${history[i]["content"]}"));
|
|
} else {
|
|
messages.insert(
|
|
0,
|
|
types.TextMessage(
|
|
author: (history[i]["role"] == "user") ? user : assistant,
|
|
id: const Uuid().v4(),
|
|
text: history[i]["content"]));
|
|
}
|
|
}
|
|
}
|
|
model = jsonDecode((prefs!.getStringList("chats") ?? [])[index])["model"];
|
|
setState(() {});
|
|
}
|
|
|
|
Future<bool> deleteChatDialog(BuildContext context, Function setState,
|
|
{bool takeAction = true,
|
|
bool? additionalCondition,
|
|
String? uuid,
|
|
bool popSidebar = false}) async {
|
|
additionalCondition ??= true;
|
|
uuid ??= chatUuid;
|
|
|
|
bool returnValue = false;
|
|
void delete(BuildContext context) {
|
|
returnValue = true;
|
|
if (takeAction) {
|
|
for (var i = 0; i < (prefs!.getStringList("chats") ?? []).length; i++) {
|
|
if (jsonDecode((prefs!.getStringList("chats") ?? [])[i])["uuid"] ==
|
|
uuid) {
|
|
List<String> tmp = prefs!.getStringList("chats")!;
|
|
tmp.removeAt(i);
|
|
prefs!.setStringList("chats", tmp);
|
|
break;
|
|
}
|
|
}
|
|
if (chatUuid == uuid) {
|
|
messages = [];
|
|
chatUuid = null;
|
|
if (!desktopLayoutRequired(context) &&
|
|
Navigator.of(context).canPop() &&
|
|
popSidebar) {
|
|
Navigator.of(context).pop();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ((prefs!.getBool("askBeforeDeletion") ?? false) && additionalCondition) {
|
|
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();
|
|
},
|
|
child: Text(
|
|
AppLocalizations.of(context)!.deleteDialogCancel)),
|
|
TextButton(
|
|
onPressed: () {
|
|
selectionHaptic();
|
|
Navigator.of(context).pop();
|
|
delete(context);
|
|
},
|
|
child: Text(
|
|
AppLocalizations.of(context)!.deleteDialogDelete))
|
|
]);
|
|
});
|
|
});
|
|
} else {
|
|
delete(context);
|
|
}
|
|
setState(() {});
|
|
return returnValue;
|
|
}
|
|
|
|
Future<String> prompt(BuildContext context,
|
|
{String description = "",
|
|
String value = "",
|
|
String title = "",
|
|
String? valueIfCanceled,
|
|
TextInputType keyboard = TextInputType.text,
|
|
bool autocorrect = true,
|
|
Iterable<String> autofillHints = const [],
|
|
bool enableSuggestions = true,
|
|
Icon? prefixIcon,
|
|
int maxLines = 1,
|
|
String? uuid,
|
|
Future<bool> Function(String content)? validator,
|
|
String? validatorError,
|
|
String? Function(String content)? validatorErrorCallback,
|
|
String? placeholder,
|
|
bool prefill = true}) async {
|
|
var returnText = (valueIfCanceled != null) ? valueIfCanceled : value;
|
|
final TextEditingController controller =
|
|
TextEditingController(text: prefill ? value : "");
|
|
bool loading = false;
|
|
String? error;
|
|
await showModalBottomSheet(
|
|
context: context,
|
|
isScrollControlled: true,
|
|
builder: (context) {
|
|
return StatefulBuilder(builder: (context, setLocalState) {
|
|
void submit() async {
|
|
selectionHaptic();
|
|
if (validator != null) {
|
|
setLocalState(() {
|
|
error = null;
|
|
loading = true;
|
|
});
|
|
bool valid = await validator(controller.text);
|
|
setLocalState(() {
|
|
loading = false;
|
|
});
|
|
if (!valid) {
|
|
setLocalState(() {
|
|
if (validatorError != null) {
|
|
error = validatorError;
|
|
} else if (validatorErrorCallback != null) {
|
|
error = validatorErrorCallback(controller.text);
|
|
} else {
|
|
error = null;
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
returnText = controller.text;
|
|
// ignore: use_build_context_synchronously
|
|
Navigator.of(context).pop();
|
|
}
|
|
|
|
return PopScope(
|
|
child: Container(
|
|
padding: EdgeInsets.only(
|
|
left: 16,
|
|
right: 16,
|
|
top: 16,
|
|
bottom: desktopFeature(web: true)
|
|
? 12
|
|
: MediaQuery.of(context).viewInsets.bottom),
|
|
width: double.infinity,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
(title != "")
|
|
? Text(title,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.bold))
|
|
: const SizedBox.shrink(),
|
|
(title != "")
|
|
? const Divider()
|
|
: const SizedBox.shrink(),
|
|
(description != "")
|
|
? Text(description)
|
|
: const SizedBox.shrink(),
|
|
const SizedBox(height: 8),
|
|
TextField(
|
|
controller: controller,
|
|
autofocus: true,
|
|
keyboardType: keyboard,
|
|
autocorrect: autocorrect,
|
|
autofillHints: autofillHints,
|
|
enableSuggestions: enableSuggestions,
|
|
maxLines: maxLines,
|
|
onSubmitted: (_) => submit(),
|
|
decoration: InputDecoration(
|
|
border: const OutlineInputBorder(),
|
|
hintText: placeholder,
|
|
errorText: error,
|
|
suffixIcon: IconButton(
|
|
enableFeedback: false,
|
|
tooltip: AppLocalizations.of(context)!
|
|
.tooltipSave,
|
|
onPressed: submit,
|
|
icon: const Icon(Icons.save_rounded)),
|
|
prefixIcon: (title ==
|
|
AppLocalizations.of(context)!
|
|
.dialogEnterNewTitle &&
|
|
uuid != null)
|
|
? IconButton(
|
|
enableFeedback: false,
|
|
tooltip: AppLocalizations.of(context)!
|
|
.tooltipLetAIThink,
|
|
onPressed: () async {
|
|
selectionHaptic();
|
|
setLocalState(() {
|
|
loading = true;
|
|
});
|
|
|
|
try {
|
|
var title = await getTitleAi(
|
|
getHistoryString(uuid));
|
|
controller.text = title;
|
|
setLocalState(() {
|
|
loading = false;
|
|
});
|
|
} catch (_) {
|
|
try {
|
|
setLocalState(() {
|
|
loading = false;
|
|
});
|
|
// ignore: use_build_context_synchronously
|
|
ScaffoldMessenger.of(context)
|
|
.showSnackBar(SnackBar(
|
|
content: Text(
|
|
AppLocalizations.of(
|
|
// ignore: use_build_context_synchronously
|
|
context)!
|
|
.settingsHostInvalid(
|
|
"timeout")),
|
|
showCloseIcon: true));
|
|
} catch (_) {}
|
|
}
|
|
},
|
|
icon: const Icon(
|
|
Icons.auto_awesome_rounded))
|
|
: prefixIcon)),
|
|
SizedBox(
|
|
height: 3,
|
|
child: (loading)
|
|
? const LinearProgressIndicator()
|
|
: const SizedBox.shrink()),
|
|
(MediaQuery.of(context).viewInsets.bottom != 0)
|
|
? const SizedBox(height: 16)
|
|
: const SizedBox.shrink()
|
|
])));
|
|
});
|
|
});
|
|
return returnText;
|
|
}
|