Added model adding

This commit is contained in:
JHubi1 2024-08-24 02:23:53 +02:00
parent daff6a722f
commit 1d3a5e91d9
No known key found for this signature in database
GPG Key ID: 7BF82570CBBBD050
5 changed files with 357 additions and 55 deletions

View File

@ -135,9 +135,82 @@
"description": "Text displayed for add model button",
"context": "Visible in the model dialog"
},
"modelDialogAddSteps": "Adding models is not supported. Go to your host pc and add models there.",
"@modelDialogAddSteps": {
"description": "Steps to add a new model",
"modelDialogAddPromptTitle": "Add new model",
"@modelDialogAddPromptTitle": {
"description": "Title of the add model dialog",
"context": "Visible in the model dialog"
},
"modelDialogAddPromptDescription": "This can have either be a normal name (e.g. 'llama3') or name and tag (e.g. 'llama3:70b').",
"@modelDialogAddPromptDescription": {
"description": "Description of the add model dialog",
"context": "Visible in the model dialog"
},
"modelDialogAddPromptAlreadyExists": "Model already exists",
"@modelDialogAddPromptAlreadyExists": {
"description": "Text displayed when the model already exists",
"context": "Visible in the model dialog"
},
"modelDialogAddPromptInvalid": "Invalid model name",
"@modelDialogAddPromptInvalid": {
"description": "Text displayed when the model name is invalid",
"context": "Visible in the model dialog"
},
"modelDialogAddAssuranceTitle": "Add {model}?",
"@modelDialogAddAssuranceTitle": {
"description": "Title of the add model assurance dialog",
"context": "Visible in the model dialog",
"placeholders": {
"model": {
"type": "String",
"description": "Name of the model to be added"
}
}
},
"modelDialogAddAssuranceDescription": "Pressing 'Add' will download the model '{model}' directly from the Ollama server to your host.\nThis can take a while depending on your internet connection. The action cannot be canceled.\nIf the app is closed during the download, it'll resume if you enter the name into the model dialog again.",
"@modelDialogAddAssuranceDescription": {
"description": "Description of the add model assurance dialog",
"context": "Visible in the model dialog",
"placeholders": {
"model": {
"type": "String",
"description": "Name of the model to be added"
}
}
},
"modelDialogAddAssuranceAdd": "Add",
"@modelDialogAddAssuranceAdd": {
"description": "Text displayed for add button, should be capitalized",
"context": "Visible in the model dialog"
},
"modelDialogAddAssuranceCancel": "Cancel",
"@modelDialogAddAssuranceCancel": {
"description": "Text displayed for cancel button, should be capitalized",
"context": "Visible in the model dialog"
},
"modelDialogAddDownloadPercentLoading": "loading progress",
"@modelDialogAddDownloadPercentLoading": {
"description": "Text displayed while loading the download progress",
"context": "Visible in the model dialog; 'loading progress in the moment'"
},
"modelDialogAddDownloadPercent": "download at {percent}%",
"@modelDialogAddDownloadPercent": {
"description": "Text displayed while downloading a model",
"context": "Visible in the model dialog; download is at x percent",
"placeholders": {
"percent": {
"type": "String",
"description": "Percentage of the download"
}
}
},
"modelDialogAddDownloadFailed": "Disconnected, try again",
"@modelDialogAddDownloadFailed": {
"description": "Text displayed when the download of a model fails",
"context": "Visible in the model dialog"
},
"modelDialogAddDownloadSuccess": "Download successful",
"@modelDialogAddDownloadSuccess": {
"description": "Text displayed when the download of a model is successful",
"context": "Visible in the model dialog"
},
"deleteDialogTitle": "Delete Chat",

View File

@ -81,6 +81,7 @@ SpeechToText speech = SpeechToText();
FlutterTts voice = FlutterTts();
bool voiceSupported = false;
BuildContext? mainContext;
void Function(void Function())? setGlobalState;
void Function(void Function())? setMainAppState;
@ -673,6 +674,7 @@ class _MainAppState extends State<MainApp> {
@override
void initState() {
super.initState();
mainContext = context;
WidgetsBinding.instance.addPostFrameCallback(
(_) async {
@ -692,6 +694,10 @@ class _MainAppState extends State<MainApp> {
}
if (!(allowSettings || useHost)) {
// ignore: use_build_context_synchronously
resetSystemNavigation(context,
statusBarColor: Colors.black,
systemNavigationBarColor: Colors.black);
showDialog(
// ignore: use_build_context_synchronously
context: context,

View File

@ -422,6 +422,7 @@ class _ScreenSettingsState extends State<ScreenSettings> {
value: (prefs!
.getString("hostHeaders") ??
""),
enableSuggestions: false,
valueIfCanceled: "{}",
validator: (content) async {
try {

View File

@ -22,6 +22,7 @@ void setModel(BuildContext context, Function setState) {
List<String> modelsReal = [];
List<bool> modal = [];
int usedIndex = -1;
int oldIndex = -1;
int addIndex = -1;
bool loaded = false;
Function? setModalState;
@ -52,6 +53,18 @@ void setModel(BuildContext context, Function setState) {
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;
@ -64,7 +77,6 @@ void setModel(BuildContext context, Function setState) {
Navigator.of(context).pop();
// ignore: use_build_context_synchronously
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
// ignore: use_build_context_synchronously
content: Text(
// ignore: use_build_context_synchronously
AppLocalizations.of(context)!.settingsHostInvalid("timeout")),
@ -84,18 +96,14 @@ void setModel(BuildContext context, Function setState) {
onPopInvoked: (didPop) 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;
}
} else {
setState(() {
desktopTitleVisible = true;
});
Navigator.of(context).pop();
return;
}
model = (usedIndex >= 0) ? modelsReal[usedIndex] : null;
chatAllowed = !(model == null);
@ -108,6 +116,7 @@ void setModel(BuildContext context, Function setState) {
prefs?.setBool("multimodal", multimodal);
if (model != null &&
preload &&
int.parse(prefs!.getString("keepAlive") ?? "300") != 0 &&
(prefs!.getBool("preloadModel") ?? true)) {
setLocalState(() {});
@ -152,7 +161,9 @@ void setModel(BuildContext context, Function setState) {
setState(() {
desktopTitleVisible = true;
});
Navigator.of(context).pop();
try {
Navigator.of(context).pop();
} catch (_) {}
}
},
child: Container(
@ -251,14 +262,16 @@ void setModel(BuildContext context, Function setState) {
onSelected: (bool selected) {
selectionHaptic();
if (addIndex == index) {
usedIndex = oldIndex;
Navigator.of(context).pop();
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(
content: Text(
AppLocalizations.of(
context)!
.modelDialogAddSteps),
showCloseIcon: true));
// ScaffoldMessenger.of(context)
// .showSnackBar(SnackBar(
// content: Text(
// AppLocalizations.of(
// context)!
// .modelDialogAddSteps),
// showCloseIcon: true));
addModel(context, setState);
}
if (!chatAllowed && model != null) {
return;
@ -299,6 +312,166 @@ void setModel(BuildContext context, Function setState) {
}
}
void addModel(BuildContext context, Function setState) async {
var client = llama.OllamaClient(
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
.cast<String, String>(),
baseUrl: "$host/api");
bool canceled = false;
bool networkError = false;
bool alreadyExists = false;
final String invalidText =
AppLocalizations.of(context)!.modelDialogAddPromptInvalid;
final networkErrorText =
AppLocalizations.of(context)!.settingsHostInvalid("other");
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;
model = model.removeSuffix(":latest");
if (model == "") return false;
canceled = false;
networkError = false;
alreadyExists = false;
try {
var request = await client.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;
}
http.Response response;
try {
response = await http
.get(Uri.parse("https://ollama.com/library/$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: 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;
}
return false;
},
validatorErrorCallback: (content) {
if (networkError) return networkErrorText;
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: 4)),
LinearProgressIndicator(value: percent),
],
)));
});
});
try {
final stream = client
.pullModelStream(request: llama.PullModelRequest(model: requestedModel))
.timeout(Duration(
seconds: (10.0 * (prefs!.getDouble("timeoutMultiplier") ?? 1.0))
.round()));
await for (final res in stream) {
percent = ((res.completed ?? 0).toInt() / (res.total ?? 100).toInt());
if ((percent * 100).round() == 0) {
percent = null;
}
setDialogState!(() {});
}
Navigator.of(mainContext!).pop();
setState(() {
model = requestedModel;
if (model!.split(":").length == 1) {
model = "$model:latest";
}
});
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++) {
@ -493,11 +666,15 @@ Future<String> prompt(BuildContext context,
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;
@ -510,6 +687,35 @@ Future<String> prompt(BuildContext 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(
@ -540,25 +746,11 @@ Future<String> prompt(BuildContext context,
controller: controller,
autofocus: true,
keyboardType: keyboard,
autocorrect: autocorrect,
autofillHints: autofillHints,
enableSuggestions: enableSuggestions,
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();
},
onSubmitted: (_) => submit(),
decoration: InputDecoration(
border: const OutlineInputBorder(),
hintText: placeholder,
@ -566,25 +758,7 @@ Future<String> prompt(BuildContext context,
suffixIcon: IconButton(
tooltip: AppLocalizations.of(context)!
.tooltipSave,
onPressed: () 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();
},
onPressed: submit,
icon: const Icon(Icons.save_rounded)),
prefixIcon: (title ==
AppLocalizations.of(context)!

View File

@ -2,6 +2,18 @@
"de": [
"deleteChat",
"renameChat",
"modelDialogAddPromptTitle",
"modelDialogAddPromptDescription",
"modelDialogAddPromptAlreadyExists",
"modelDialogAddPromptInvalid",
"modelDialogAddAssuranceTitle",
"modelDialogAddAssuranceDescription",
"modelDialogAddAssuranceAdd",
"modelDialogAddAssuranceCancel",
"modelDialogAddDownloadPercentLoading",
"modelDialogAddDownloadPercent",
"modelDialogAddDownloadFailed",
"modelDialogAddDownloadSuccess",
"settingsDescriptionBehavior",
"settingsDescriptionInterface",
"settingsDescriptionVoice",
@ -27,6 +39,18 @@
"it": [
"deleteChat",
"renameChat",
"modelDialogAddPromptTitle",
"modelDialogAddPromptDescription",
"modelDialogAddPromptAlreadyExists",
"modelDialogAddPromptInvalid",
"modelDialogAddAssuranceTitle",
"modelDialogAddAssuranceDescription",
"modelDialogAddAssuranceAdd",
"modelDialogAddAssuranceCancel",
"modelDialogAddDownloadPercentLoading",
"modelDialogAddDownloadPercent",
"modelDialogAddDownloadFailed",
"modelDialogAddDownloadSuccess",
"settingsDescriptionBehavior",
"settingsDescriptionInterface",
"settingsDescriptionVoice",
@ -52,6 +76,18 @@
"tr": [
"deleteChat",
"renameChat",
"modelDialogAddPromptTitle",
"modelDialogAddPromptDescription",
"modelDialogAddPromptAlreadyExists",
"modelDialogAddPromptInvalid",
"modelDialogAddAssuranceTitle",
"modelDialogAddAssuranceDescription",
"modelDialogAddAssuranceAdd",
"modelDialogAddAssuranceCancel",
"modelDialogAddDownloadPercentLoading",
"modelDialogAddDownloadPercent",
"modelDialogAddDownloadFailed",
"modelDialogAddDownloadSuccess",
"settingsDescriptionBehavior",
"settingsDescriptionInterface",
"settingsDescriptionVoice",
@ -77,6 +113,18 @@
"zh": [
"deleteChat",
"renameChat",
"modelDialogAddPromptTitle",
"modelDialogAddPromptDescription",
"modelDialogAddPromptAlreadyExists",
"modelDialogAddPromptInvalid",
"modelDialogAddAssuranceTitle",
"modelDialogAddAssuranceDescription",
"modelDialogAddAssuranceAdd",
"modelDialogAddAssuranceCancel",
"modelDialogAddDownloadPercentLoading",
"modelDialogAddDownloadPercent",
"modelDialogAddDownloadFailed",
"modelDialogAddDownloadSuccess",
"settingsDescriptionBehavior",
"settingsDescriptionInterface",
"settingsDescriptionVoice",