Files
ollama-android-ui-Maid/lib/pages/home_page.dart
2023-11-11 13:16:14 +10:00

342 lines
12 KiB
Dart

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:maid/utilities/generation_manager.dart';
import 'package:maid/utilities/host.dart';
import 'package:maid/utilities/memory_manager.dart';
import 'package:maid/utilities/message_manager.dart';
import 'package:maid/widgets/settings_widgets/maid_text_field.dart';
import 'package:system_info2/system_info2.dart';
import 'package:maid/utilities/model.dart';
import 'package:maid/core/local_generation.dart';
import 'package:maid/pages/character_page.dart';
import 'package:maid/pages/model_page.dart';
import 'package:maid/pages/settings_page.dart';
import 'package:maid/pages/about_page.dart';
import 'package:maid/widgets/chat_widgets/chat_message.dart';
class MaidHomePage extends StatefulWidget {
final String title;
const MaidHomePage({super.key, required this.title});
@override
MaidHomePageState createState() => MaidHomePageState();
}
class MaidHomePageState extends State<MaidHomePage> {
final ScrollController _consoleScrollController = ScrollController();
TextEditingController promptController = TextEditingController();
static int ram = SysInfo.getTotalPhysicalMemory() ~/ (1024 * 1024 * 1024);
List<ChatMessage> chatWidgets = [];
void _missingModelDialog() {
// Use a local reference to context to avoid using it across an async gap.
final localContext = context;
// Ensure that the context is still valid before attempting to show the dialog.
if (localContext.mounted) {
showDialog(
context: localContext,
builder: (BuildContext context) {
return AlertDialog(
title: const Text(
"Model Missing\nPlease assign a model in model settings.",
textAlign: TextAlign.center,
),
alignment: Alignment.center,
actionsAlignment: MainAxisAlignment.center,
backgroundColor: Theme.of(context).colorScheme.background,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(20.0)),
),
actions: [
FilledButton(
onPressed: () {
Navigator.of(context).pop();
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const ModelPage()));
},
child: Text("Open Model Settings",
style: Theme.of(context).textTheme.labelLarge),
),
FilledButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text("Close",
style: Theme.of(context).textTheme.labelLarge),
),
],
);
},
);
setState(() {});
}
}
void send() {
MemoryManager.asave().then((value) {
if (Platform.isAndroid || Platform.isIOS) {
FocusScope.of(context).unfocus();
}
MessageManager.add(UniqueKey(),
message: promptController.text.trim(),
userGenerated: true
);
MessageManager.add(UniqueKey());
if (MemoryManager.checkFileExists(model.parameters["path"])) {
GenerationManager.prompt(promptController.text.trim());
setState(() {
model.busy = true;
promptController.clear();
});
} else {
_missingModelDialog();
setState(() {
model.busy = false;
promptController.clear();
});
}
});
}
void updateCallback() {
chatWidgets.clear();
Map<Key, bool> history = MessageManager.history();
for (var key in history.keys) {
chatWidgets.add(ChatMessage(
key: key,
userGenerated: history[key] ?? false,
));
}
_consoleScrollController.animateTo(
_consoleScrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 50),
curve: Curves.easeOut,
);
setState(() {});
}
@override
void initState() {
super.initState();
MessageManager.registerCallback(updateCallback);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
elevation: 0.0,
centerTitle: true,
flexibleSpace: Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
),
),
title: Text(widget.title),
),
drawer: Drawer(
child: ListView(
padding: EdgeInsets.zero,
children: [
const SizedBox(
height: 50,
),
Text(
ram == -1 ? 'RAM: Unknown' : 'RAM: $ram GB',
textAlign: TextAlign.center,
style: TextStyle(
color:
Color.lerp(Colors.red, Colors.green, ram.clamp(0, 8) / 8) ??
Colors.red,
fontSize: 15,
),
),
Divider(
indent: 10,
endIndent: 10,
color: Theme.of(context).colorScheme.onPrimary,
),
ListTile(
leading: Icon(Icons.person,
color: Theme.of(context).colorScheme.onPrimary),
title: Text('Character',
style: Theme.of(context).textTheme.labelLarge),
onTap: () {
Navigator.pop(context); // Close the drawer
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CharacterPage()));
},
),
ListTile(
leading: Icon(Icons.account_tree_rounded,
color: Theme.of(context).colorScheme.onPrimary),
title: Text(
'Model',
style: Theme.of(context).textTheme.labelLarge,
),
onTap: () {
Navigator.pop(context); // Close the drawer
Navigator.push(context,
MaterialPageRoute(builder: (context) => const ModelPage()));
},
),
ListTile(
leading: Icon(Icons.settings,
color: Theme.of(context).colorScheme.onPrimary),
title: Text('Settings',
style: Theme.of(context).textTheme.labelLarge),
onTap: () {
Navigator.pop(context); // Close the drawer
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsPage()));
},
),
ListTile(
leading: Icon(Icons.info,
color: Theme.of(context).colorScheme.onPrimary),
title:
Text('About', style: Theme.of(context).textTheme.labelLarge),
onTap: () {
Navigator.pop(context); // Close the drawer
Navigator.push(context,
MaterialPageRoute(builder: (context) => const AboutPage()));
},
),
Divider(
indent: 10,
endIndent: 10,
color: Theme.of(context).colorScheme.onPrimary,
),
SwitchListTile(
title: const Text('Local / Remote'),
value: GenerationManager.remote,
onChanged: (value) {
setState(() {
GenerationManager.remote = value;
});
},
),
if (GenerationManager.remote)
ListTile(
title: TextField(
cursorColor: Theme.of(context).colorScheme.secondary,
controller: Host.urlController,
decoration: const InputDecoration(
labelText: 'URL',
),
),
),
],
),
),
body: Builder(
builder: (BuildContext context) => GestureDetector(
onHorizontalDragEnd: (details) {
// Check if the drag is towards right with a certain velocity
if (details.primaryVelocity! > 100) {
// Open the drawer
Scaffold.of(context).openDrawer();
}
},
child: Stack(
children: [
Container(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.background,
),
),
Column(
children: [
Expanded(
child: ListView.builder(
controller: _consoleScrollController,
itemCount: chatWidgets.length,
itemBuilder: (BuildContext context, int index) {
return chatWidgets[index];
},
),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
children: [
if (model.busy && !GenerationManager.remote)
IconButton(
onPressed: LocalGeneration.instance.stop,
iconSize: 50,
icon: const Icon(
Icons.stop_circle_sharp,
color: Colors.red,
)),
Expanded(
child: TextField(
keyboardType: TextInputType.multiline,
minLines: 1,
maxLines: 9,
enableInteractiveSelection: true,
onSubmitted: (value) {
if (!model.busy) {
if (model.parameters["path"]
.toString()
.isEmpty) {
_missingModelDialog();
} else {
send();
}
}
},
controller: promptController,
cursorColor:
Theme.of(context).colorScheme.secondary,
decoration: InputDecoration(
labelText: 'Prompt',
hintStyle:
Theme.of(context).textTheme.labelSmall),
),
),
IconButton(
onPressed: () {
if (!model.busy) {
if (model.parameters["path"]
.toString()
.isEmpty) {
_missingModelDialog();
} else {
send();
}
}
},
iconSize: 50,
icon: Icon(
Icons.arrow_circle_right,
color: model.busy
? Theme.of(context).colorScheme.onPrimary
: Theme.of(context).colorScheme.secondary,
)),
],
),
),
],
),
],
),
),
),
);
}
}