Markdown renderer, other changes
This commit is contained in:
parent
83ba8bd15f
commit
ba63e0fc50
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:ollama_dart/ollama_dart.dart';
|
||||
import 'package:scroll_to_index/scroll_to_index.dart';
|
||||
|
||||
// import 'package:flutter_chat_types/flutter_chat_types.dart' as types;
|
||||
// import 'package:flutter_chat_ui/flutter_chat_ui.dart' as chat_ui;
|
||||
|
|
@ -18,8 +19,8 @@ import '../l10n/gen/app_localizations.dart';
|
|||
import '../main.dart';
|
||||
import '../services/chat.dart';
|
||||
import '../services/error.dart';
|
||||
import '../services/markdown.dart';
|
||||
import '../services/model.dart';
|
||||
import '../services/theme.dart';
|
||||
// import '../services/model.dart';
|
||||
// import '../services/preferences.dart';
|
||||
// import '../worker/desktop.dart';
|
||||
|
|
@ -1834,8 +1835,6 @@ class _ScreenMainState extends State<ScreenMain> {
|
|||
return Scaffold(
|
||||
body: ListView(
|
||||
children: [
|
||||
const ListTile(title: ThemeModeSwitch()),
|
||||
const ListTile(title: ThemeSwitch()),
|
||||
...ChatManager.instance.chats.map(
|
||||
(chat) => ScreenMainChatTile(chat: chat),
|
||||
),
|
||||
|
|
@ -1853,7 +1852,7 @@ class _ScreenMainState extends State<ScreenMain> {
|
|||
"M49WC9CW",
|
||||
() async {
|
||||
var msg = TextMessage(
|
||||
"Hi!", // "Hello! Please explain what you can do.",
|
||||
"Write a long demo message for all GitHub Flavored Markdown features.",
|
||||
sender: MessageSender.user,
|
||||
);
|
||||
return chat.send(msg);
|
||||
|
|
@ -1916,11 +1915,20 @@ class _ScreenMainChatTileState extends State<ScreenMainChatTile> {
|
|||
widget.chat.title.emptyOn(AppLocalizations.of(context).newChatTitle),
|
||||
placeholder: Text(AppLocalizations.of(context).newChatTitle),
|
||||
),
|
||||
subtitle: (widget.chat.messages.isEmpty)
|
||||
? const SizedBox.shrink()
|
||||
: ChatText(
|
||||
(widget.chat.messages.last as TextMessage).content,
|
||||
placeholder: const Text("<incoming>"),
|
||||
subtitle:
|
||||
(widget.chat.messages.isEmpty ||
|
||||
widget.chat.messages.last.runtimeType != TextMessage)
|
||||
? null
|
||||
// : ChatText(
|
||||
// (widget.chat.messages.last as TextMessage).content,
|
||||
// placeholder: const Text("<incoming>"),
|
||||
// ),
|
||||
: (widget.chat.messages.last as TextMessage).content.isEmpty
|
||||
? const LinearProgressIndicator()
|
||||
: Text.rich(
|
||||
Markdown(
|
||||
(widget.chat.messages.last as TextMessage).content,
|
||||
).toTextSpan(context),
|
||||
),
|
||||
onTap: () => ChatManager.instance.deleteChat(widget.chat),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ import 'preferences.dart';
|
|||
|
||||
enum MessageSender { user, assistant }
|
||||
|
||||
class Message extends ChangeNotifier {
|
||||
abstract class Message extends ChangeNotifier {
|
||||
final String id;
|
||||
|
||||
final MessageSender sender;
|
||||
|
|
@ -27,17 +27,13 @@ class Message extends ChangeNotifier {
|
|||
|
||||
void modify() => notifyListeners();
|
||||
|
||||
Map<String, dynamic> toJson() =>
|
||||
throw UnimplementedError("toJson() must be implemented in subclasses");
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
@override
|
||||
String toString() => toJson().toString();
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
return other is Message && other.id == id;
|
||||
}
|
||||
|
||||
operator ==(Object other) => other is Message && other.id == id;
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
|
@ -75,10 +71,8 @@ class TextMessage extends Message {
|
|||
required MessageSender sender,
|
||||
Completer<void>? completer,
|
||||
void Function(String content)? onContent,
|
||||
}) {
|
||||
return TextMessage("", sender: sender, createdAt: DateTime.now())
|
||||
.._contentFromStream(stream, completer: completer, onContent: onContent);
|
||||
}
|
||||
}) => TextMessage("", sender: sender, createdAt: DateTime.now())
|
||||
.._contentFromStream(stream, completer: completer, onContent: onContent);
|
||||
|
||||
Future<void> _contentFromStream(
|
||||
Stream<ollama.GenerateChatCompletionResponse> stream, {
|
||||
|
|
@ -89,14 +83,19 @@ class TextMessage extends Message {
|
|||
_locked = true;
|
||||
|
||||
try {
|
||||
await for (var response in stream) {
|
||||
if (completer?.isCompleted ?? false) return;
|
||||
_content += response.message.content;
|
||||
StreamSubscription? sub;
|
||||
sub = stream.listen((event) {
|
||||
if (completer?.isCompleted ?? false) {
|
||||
sub?.cancel();
|
||||
return;
|
||||
}
|
||||
_content += event.message.content;
|
||||
chatHaptic();
|
||||
|
||||
notifyListeners();
|
||||
onContent?.call(_content);
|
||||
}
|
||||
});
|
||||
await sub.asFuture();
|
||||
} catch (e, s) {
|
||||
if (completer?.isCompleted ?? false) return;
|
||||
completer?.completeError(e, s);
|
||||
|
|
@ -132,7 +131,7 @@ class ImageMessage extends Message {
|
|||
}
|
||||
|
||||
class Chat extends ChangeNotifier {
|
||||
Completer<void>? completer;
|
||||
Completer<void> completer = Completer<void>()..complete();
|
||||
|
||||
bool get alive => ChatManager.instance.chats.contains(this);
|
||||
bool get active => alive && ChatManager.instance.currentChatId == id;
|
||||
|
|
@ -188,6 +187,18 @@ class Chat extends ChangeNotifier {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
for (var message in _messages) {
|
||||
message.removeListener(notifyListeners);
|
||||
}
|
||||
if (completer.isCompleted == false) completer.complete();
|
||||
if (ChatManager.instance.currentChatId == id) {
|
||||
ChatManager.instance.currentChatId = null;
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
List<ollama.Message> toApi() {
|
||||
var messages = <ollama.Message>[];
|
||||
var images = <ImageMessage>[];
|
||||
|
|
@ -288,6 +299,8 @@ class Chat extends ChangeNotifier {
|
|||
if (alive) Error.throwWithStackTrace(e, s);
|
||||
return;
|
||||
}
|
||||
if (!alive) return;
|
||||
|
||||
var newTitle = generated.message.content;
|
||||
newTitle = newTitle.replaceAll("\n", " ");
|
||||
|
||||
|
|
@ -321,17 +334,29 @@ class Chat extends ChangeNotifier {
|
|||
title = newTitle.trim();
|
||||
}
|
||||
|
||||
void append(Message message) {
|
||||
assert(alive, "Chat must be alive to be modified.");
|
||||
|
||||
_messages.add(message..addListener(notifyListeners));
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> send(
|
||||
Message message, {
|
||||
bool awaitCompletion = true,
|
||||
bool? think,
|
||||
void Function(String)? onContent,
|
||||
void Function(String content)? onContent,
|
||||
}) async {
|
||||
assert(alive, "Chat must be alive to be modified.");
|
||||
|
||||
assert(model != null, "Chat model must be set to send messages.");
|
||||
if (model == null) return;
|
||||
|
||||
assert(
|
||||
completer.isCompleted,
|
||||
"Cannot send a message while another message is being sent.",
|
||||
);
|
||||
|
||||
_messages.add(message..addListener(notifyListeners));
|
||||
|
||||
var finalThink =
|
||||
|
|
@ -356,14 +381,13 @@ class Chat extends ChangeNotifier {
|
|||
)..addListener(notifyListeners);
|
||||
_messages.add(message);
|
||||
|
||||
var future = completer!.future
|
||||
.then((_) => ChatManager.instance.saveChats())
|
||||
.catchError((e, s) {
|
||||
message
|
||||
.._includesError = true
|
||||
..modify();
|
||||
Error.throwWithStackTrace(e, s);
|
||||
});
|
||||
var future = completer.future.catchError((e, s) {
|
||||
message
|
||||
.._locked = false
|
||||
.._includesError = true
|
||||
..modify();
|
||||
Error.throwWithStackTrace(e, s);
|
||||
});
|
||||
if (awaitCompletion) await future;
|
||||
}
|
||||
|
||||
|
|
@ -378,6 +402,11 @@ class Chat extends ChangeNotifier {
|
|||
message.removeListener(notifyListeners);
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) => other is Chat && other.id == id;
|
||||
@override
|
||||
int get hashCode => id.hashCode;
|
||||
}
|
||||
|
||||
class ChatManager extends ChangeNotifier {
|
||||
|
|
@ -407,6 +436,15 @@ class ChatManager extends ChangeNotifier {
|
|||
|
||||
ChatManager._();
|
||||
|
||||
void clearChats() {
|
||||
for (var chat in _chats) {
|
||||
chat.dispose();
|
||||
}
|
||||
_chats.clear();
|
||||
_currentChatId = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadChats() async {
|
||||
DateTime getDateTimeFromMilliseconds(int? milliseconds) {
|
||||
if (milliseconds == null) return DateTime.now();
|
||||
|
|
@ -502,8 +540,11 @@ class ChatManager extends ChangeNotifier {
|
|||
|
||||
void deleteChat(Chat chat) {
|
||||
_chats.remove(chat);
|
||||
if (chat.completer?.isCompleted == false) chat.completer!.complete();
|
||||
chat.removeListener(notifyListeners);
|
||||
for (var message in chat.messages) {
|
||||
message.removeListener(chat.notifyListeners);
|
||||
}
|
||||
if (chat.completer.isCompleted == false) chat.completer.complete();
|
||||
|
||||
if (_currentChatId == chat.id) _currentChatId = null;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,17 +6,21 @@ import 'package:ollama_dart/ollama_dart.dart' as llama;
|
|||
|
||||
import '../main.dart';
|
||||
|
||||
const String userAgent = "Ollama App";
|
||||
|
||||
class OllamaHttpOverrides extends HttpOverrides {
|
||||
@override
|
||||
HttpClient createHttpClient(SecurityContext? context) {
|
||||
return super.createHttpClient(context)
|
||||
..userAgent = userAgent
|
||||
..badCertificateCallback = (_, __, ___) => true;
|
||||
}
|
||||
}
|
||||
|
||||
final httpClient = http.Client();
|
||||
llama.OllamaClient get ollamaClient => llama.OllamaClient(
|
||||
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
|
||||
.cast<String, String>(),
|
||||
baseUrl: "$host/api",
|
||||
client: httpClient);
|
||||
headers: (jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map)
|
||||
.cast<String, String>(),
|
||||
baseUrl: "$host/api",
|
||||
client: httpClient,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -3,16 +3,20 @@ import 'dart:io';
|
|||
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:http/http.dart';
|
||||
import 'package:ollama_dart/ollama_dart.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
import '../l10n/gen/app_localizations.dart';
|
||||
import 'markdown.dart';
|
||||
|
||||
final _logIdRegex = RegExp(r"^[A-Z0-9]{2,16}$");
|
||||
|
||||
String _removeNewlines(String content) =>
|
||||
content.replaceAll(RegExp(r"\s*\n\s*"), " ");
|
||||
|
||||
typedef ErrorGuardErrorMessageGenerator = String Function(Object exception);
|
||||
typedef ErrorGuardDetailsMessageGenerator =
|
||||
String? Function(Object exception, StackTrace stackTrace);
|
||||
|
|
@ -24,7 +28,9 @@ String _defaultErrorMessage(Object exception) => switch (exception) {
|
|||
exception.toString().split(": ").elementAtOrNull(4) ??
|
||||
"An assertion failed",
|
||||
TimeoutException _ => "Request timed out",
|
||||
SocketException _ || HttpException _ => "Could not connect to server",
|
||||
SocketException _ ||
|
||||
HttpException _ ||
|
||||
ClientException _ => "Could not connect to server",
|
||||
TlsException _ => "Could not establish secure connection",
|
||||
StateError _ => "Invalid state encountered",
|
||||
_ => "An unknown error occurred",
|
||||
|
|
@ -34,9 +40,13 @@ String? _defaultDetailsMessage(
|
|||
StackTrace stackTrace,
|
||||
) => switch (exception) {
|
||||
OllamaClientException e =>
|
||||
e.body.toString().startsWith("ClientException with SocketException")
|
||||
[
|
||||
"SocketException",
|
||||
"HttpException",
|
||||
"ClientException",
|
||||
].contains(e.body.toString().split(":").first)
|
||||
? "A network error occurred while trying to connect to the server."
|
||||
"\n\nYou may check your network connection or server reachability and try again."
|
||||
"\n\nYou may check your network connection and server reachability and try again."
|
||||
: "The Ollama API client received a faulty response with code `${e.code}`."
|
||||
"\n\nPlease check your Ollama server or proxy configuration and try again.",
|
||||
AssertionError _ =>
|
||||
|
|
@ -46,9 +56,9 @@ String? _defaultDetailsMessage(
|
|||
TimeoutException _ =>
|
||||
"Time ran out while waiting for a response from the server."
|
||||
"\n\nThis might be caused by a slow or unresponsive server, or a network issue.\nYou may try increasing the Timeout Multiplier in the settings.",
|
||||
SocketException _ || HttpException _ =>
|
||||
"A ${exception.runtimeType.toString().split(RegExp(r"(?=[A-Z])")).join(" ").toLowerCase()} might be caused by a slow or unresponsive server, or a network issue."
|
||||
"\n\nYou may check your network connection and try again.",
|
||||
SocketException _ || HttpException _ || ClientException _ =>
|
||||
"A ${exception.toString().split(":").first} might be caused by a slow or unresponsive server, or a network issue."
|
||||
"\n\nYou may check your network connection and server reachability and try again.",
|
||||
TlsException _ =>
|
||||
"An error occurred while trying to establish a secure connection via TLS."
|
||||
"\n\nThis might be caused by an invalid or expired certificate, though this should not happen. Please report this issue to the developers.",
|
||||
|
|
@ -254,7 +264,9 @@ Future<T?> errorGuard<T>(
|
|||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
_removeNewlines(errorMessageText),
|
||||
_removeNewlines(
|
||||
Markdown(errorMessageText).toTextSpan(context).toPlainText(),
|
||||
),
|
||||
style: TextStyle(color: colorScheme.onErrorContainer),
|
||||
),
|
||||
backgroundColor: colorScheme.errorContainer,
|
||||
|
|
@ -273,9 +285,6 @@ Future<T?> errorGuard<T>(
|
|||
}
|
||||
}
|
||||
|
||||
String _removeNewlines(String content) =>
|
||||
content.replaceAll(RegExp(r"\s*\n\s*"), " ");
|
||||
|
||||
class _ErrorGuardDetailsDialog extends StatefulWidget {
|
||||
final String? logId;
|
||||
final DateTime dateTime;
|
||||
|
|
@ -338,7 +347,7 @@ class _ErrorGuardDetailsDialogState extends State<_ErrorGuardDetailsDialog> {
|
|||
child: Text("@${widget.logId}"),
|
||||
),
|
||||
|
||||
Text.rich(_contentFormat(context, widget.errorMessage)),
|
||||
Text.rich(Markdown(widget.errorMessage).toTextSpan(context)),
|
||||
const SizedBox(height: 8),
|
||||
if (widget.detailsMessage != null) ...[
|
||||
_ErrorGuardDetailsPanel(
|
||||
|
|
@ -390,167 +399,6 @@ class _ErrorGuardDetailsDialogState extends State<_ErrorGuardDetailsDialog> {
|
|||
);
|
||||
}
|
||||
|
||||
static TextSpan _contentFormat(BuildContext context, String content) {
|
||||
content = content.trim().split("\n").map((l) => l.trim()).join("\n");
|
||||
if (content.isEmpty) return const TextSpan(text: "");
|
||||
// content = content.replaceAll(RegExp(r"(?=)"), "\u{00AD}");
|
||||
|
||||
var paragraphs = content.split("\n\n").map((p) => p.trim()).toList();
|
||||
|
||||
InlineSpan parseInline(String text) {
|
||||
var root = <String, dynamic>{
|
||||
"type": "root",
|
||||
"children": <Map<String, dynamic>>[],
|
||||
};
|
||||
var stack = <Map<String, dynamic>>[root];
|
||||
var buf = StringBuffer();
|
||||
|
||||
void flushBufferToCurrent() {
|
||||
if (buf.isEmpty) return;
|
||||
var textNode = {"type": "text", "text": buf.toString()};
|
||||
(stack.last["children"] as List).add(textNode);
|
||||
buf.clear();
|
||||
}
|
||||
|
||||
var i = 0;
|
||||
while (i < text.length) {
|
||||
var ch = text[i];
|
||||
if (ch == "\\" && i + 1 < text.length) {
|
||||
buf.write(text[i + 1]);
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
var inCode = stack.last["type"] == "code";
|
||||
if (inCode) {
|
||||
if (ch == "`") {
|
||||
flushBufferToCurrent();
|
||||
stack.removeLast();
|
||||
i++;
|
||||
} else {
|
||||
buf.write(ch);
|
||||
i++;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == '<') {
|
||||
var endIndex = text.indexOf('>', i + 1);
|
||||
if (endIndex != -1) {
|
||||
var url = text.substring(i + 1, endIndex);
|
||||
if (url.startsWith('https://') || url.startsWith('http://')) {
|
||||
flushBufferToCurrent();
|
||||
var linkNode = {"type": "link", "url": url};
|
||||
(stack.last["children"] as List).add(linkNode);
|
||||
i = endIndex + 1;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (ch == "`") {
|
||||
flushBufferToCurrent();
|
||||
var codeNode = {"type": "code", "children": <Map<String, dynamic>>[]};
|
||||
(stack.last["children"] as List).add(codeNode);
|
||||
stack.add(codeNode);
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (ch == "*") {
|
||||
flushBufferToCurrent();
|
||||
if (stack.last["type"] == "italic") {
|
||||
stack.removeLast();
|
||||
} else {
|
||||
var n = {"type": "italic", "children": <Map<String, dynamic>>[]};
|
||||
(stack.last['children'] as List).add(n);
|
||||
stack.add(n);
|
||||
}
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
buf.write(ch);
|
||||
i++;
|
||||
}
|
||||
|
||||
flushBufferToCurrent();
|
||||
|
||||
InlineSpan build(Map<String, dynamic> node) {
|
||||
var type = node["type"] as String;
|
||||
if (type == "text") {
|
||||
return TextSpan(text: node["text"] as String);
|
||||
}
|
||||
|
||||
if (type == "link") {
|
||||
var url = node["url"] as String;
|
||||
return TextSpan(
|
||||
text: url,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
decoration: TextDecoration.underline,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () => launchUrl(Uri.parse(url)),
|
||||
);
|
||||
}
|
||||
|
||||
var children = (node["children"] as List)
|
||||
.map<InlineSpan>((c) => build(c as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
switch (type) {
|
||||
case "root":
|
||||
return TextSpan(children: children);
|
||||
case "italic":
|
||||
return TextSpan(
|
||||
children: children,
|
||||
style: const TextStyle(fontStyle: FontStyle.italic),
|
||||
);
|
||||
case "code":
|
||||
var codeText = (node["children"] as List)
|
||||
.where((c) => (c as Map<String, dynamic>)["type"] == "text")
|
||||
.map((c) => (c as Map<String, dynamic>)["text"] as String)
|
||||
.join();
|
||||
var widget = DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).dividerColor),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2),
|
||||
child: Text(
|
||||
codeText,
|
||||
style: const TextStyle(fontFamily: "monospace"),
|
||||
),
|
||||
),
|
||||
);
|
||||
return WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
child: widget,
|
||||
);
|
||||
default:
|
||||
return TextSpan(children: children);
|
||||
}
|
||||
}
|
||||
|
||||
return build(root);
|
||||
}
|
||||
|
||||
return TextSpan(
|
||||
children: List.generate(paragraphs.length * 2 - 1, (index) {
|
||||
if (index.isOdd) {
|
||||
return const TextSpan(
|
||||
text: "\n\n",
|
||||
style: TextStyle(height: 0.5, color: Colors.transparent),
|
||||
);
|
||||
}
|
||||
var text = paragraphs[index ~/ 2];
|
||||
return parseInline(text);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
String _reportText() =>
|
||||
"""
|
||||
An exception was thrown during the execution of the app.
|
||||
|
|
@ -590,7 +438,7 @@ The app suggested the following cause of the issue:
|
|||
url += "&context=${Uri.encodeComponent(contextText)}";
|
||||
|
||||
Clipboard.setData(ClipboardData(text: url));
|
||||
launchUrl(Uri.parse(url));
|
||||
launchUrl(Uri.parse(url), mode: LaunchMode.inAppBrowserView);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -680,7 +528,7 @@ class _ErrorGuardDetailsPanelState extends State<_ErrorGuardDetailsPanel>
|
|||
leading: widget.icon,
|
||||
title: Text(widget.title),
|
||||
trailing: ExpandIcon(
|
||||
onPressed: (_) => _toggleAnimation(),
|
||||
onPressed: null,
|
||||
isExpanded: _expanded,
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
|
|
@ -706,11 +554,7 @@ class _ErrorGuardDetailsPanelState extends State<_ErrorGuardDetailsPanel>
|
|||
),
|
||||
)
|
||||
: Text.rich(
|
||||
_ErrorGuardDetailsDialogState._contentFormat(
|
||||
context,
|
||||
widget.content.trim(),
|
||||
),
|
||||
textAlign: TextAlign.justify,
|
||||
Markdown(widget.content.trim()).toTextSpan(context),
|
||||
style: TextStyle(
|
||||
fontStyle: widget.italic ? FontStyle.italic : null,
|
||||
color: widget.italic ? theme.disabledColor : null,
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -1,15 +1,15 @@
|
|||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:ollama_dart/ollama_dart.dart' as ollama;
|
||||
|
||||
import '../main.dart';
|
||||
import 'clients.dart' as clients;
|
||||
import 'preferences.dart';
|
||||
|
||||
typedef ModelCapability = ollama.Capability;
|
||||
|
||||
class Model {
|
||||
class Model extends ChangeNotifier {
|
||||
final String name;
|
||||
|
||||
String _family;
|
||||
|
|
@ -50,6 +50,7 @@ class Model {
|
|||
_families = data.details!.families?.toSet() ?? {};
|
||||
}
|
||||
_capabilities = data.capabilities!.toSet();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loadIntoMemory() async {
|
||||
|
|
@ -59,12 +60,9 @@ class Model {
|
|||
|
||||
var headers = <String, String>{
|
||||
"Content-Type": "application/json",
|
||||
...(jsonDecode(prefs!.getString("hostHeaders") ?? "{}") as Map),
|
||||
};
|
||||
var body = {
|
||||
"model": name,
|
||||
"keep_alive": int.parse(prefs!.getString("keepAlive") ?? "300"),
|
||||
...Preferences.instance.hostHeaders,
|
||||
};
|
||||
var body = {"model": name, "keep_alive": Preferences.instance.keepAlive};
|
||||
|
||||
await clients.httpClient
|
||||
.post(
|
||||
|
|
@ -76,10 +74,7 @@ class Model {
|
|||
}
|
||||
|
||||
@override
|
||||
operator ==(Object other) {
|
||||
return other is Model && other.name == name;
|
||||
}
|
||||
|
||||
operator ==(Object other) => other is Model && other.name == name;
|
||||
@override
|
||||
int get hashCode => name.hashCode;
|
||||
}
|
||||
|
|
@ -111,9 +106,14 @@ class ModelManager extends ChangeNotifier {
|
|||
|
||||
Future<void> loadModels({bool fetchCapabilitiesInBackground = true}) async {
|
||||
var data = await clients.ollamaClient.listModels();
|
||||
|
||||
for (var model in _models) {
|
||||
model.dispose();
|
||||
}
|
||||
_models.clear();
|
||||
|
||||
for (var model in data.models!) {
|
||||
_models.add(Model.fromApi(model: model));
|
||||
_models.add(Model.fromApi(model: model)..addListener(notifyListeners));
|
||||
}
|
||||
|
||||
_initialized = true;
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ class Preferences extends ChangeNotifier {
|
|||
|
||||
Map<String, String> get hostHeaders =>
|
||||
(jsonDecode(prefs?.getString("hostHeaders") ?? "{}") as Map).cast();
|
||||
set hostHeaders(Map value) {
|
||||
set hostHeaders(Map<String, String> value) {
|
||||
if (value.isEmpty) {
|
||||
prefs?.remove("hostHeaders");
|
||||
} else {
|
||||
|
|
@ -137,4 +137,10 @@ class TimeoutMultiplier {
|
|||
|
||||
/// Very long time interval. Equals 60 seconds with default multiplier.
|
||||
static Duration get veryLong => calculate(const Duration(seconds: 60));
|
||||
|
||||
/// Interval intended for streams. Equals 5 minutes with default multiplier.
|
||||
///
|
||||
/// This one might not be suitable for all models and situations. Use only
|
||||
/// if required.
|
||||
static Duration get stream => calculate(const Duration(minutes: 5));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,8 +12,8 @@ typedef ThemeBuilderBuilder =
|
|||
);
|
||||
|
||||
class ThemeBuilderData {
|
||||
ThemeBuilderData? _current;
|
||||
ThemeBuilderData? get current => _current;
|
||||
static ThemeBuilderData? _current;
|
||||
static ThemeBuilderData? get current => _current;
|
||||
|
||||
final ColorScheme? dynamicLight;
|
||||
final ColorScheme? dynamicDark;
|
||||
|
|
@ -67,7 +67,7 @@ class ThemeBuilderData {
|
|||
dynamicSchemeVariant: DynamicSchemeVariant.content,
|
||||
brightness: Brightness.dark,
|
||||
).copyWith(surface: Colors.black),
|
||||
);
|
||||
).copyWith(dividerColor: Colors.white30);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,45 @@
|
|||
import 'package:flutter/material.dart';
|
||||
|
||||
// idk why this fixes the cut off on the bottom, but it does, so anyways
|
||||
Tween<Rect?> heroTextCreateRectTween(Rect? begin, Rect? end) =>
|
||||
RectTween(begin: begin, end: end);
|
||||
|
||||
Widget heroTextFlightShuttleBuilder(
|
||||
BuildContext flightContext,
|
||||
Animation<double> animation,
|
||||
HeroFlightDirection flightDirection,
|
||||
BuildContext fromHeroContext,
|
||||
BuildContext toHeroContext,
|
||||
) {
|
||||
var fromText = (fromHeroContext.widget as Hero).child as Text;
|
||||
var toText = (toHeroContext.widget as Hero).child as Text;
|
||||
|
||||
var begin = fromText.style ?? DefaultTextStyle.of(fromHeroContext).style;
|
||||
var end = toText.style ?? DefaultTextStyle.of(toHeroContext).style;
|
||||
|
||||
if (flightDirection == HeroFlightDirection.pop) {
|
||||
(fromText, toText) = (toText, fromText);
|
||||
(begin, end) = (end, begin);
|
||||
}
|
||||
|
||||
var styleTween = TextStyleTween(begin: begin, end: end);
|
||||
var textString = fromText.data ?? toText.data ?? "";
|
||||
|
||||
return AnimatedBuilder(
|
||||
animation: animation,
|
||||
builder: (context, child) {
|
||||
var currentStyle = styleTween.lerp(animation.value);
|
||||
return Material(
|
||||
type: MaterialType.transparency,
|
||||
child: Center(
|
||||
child: Text(
|
||||
textString,
|
||||
textAlign: TextAlign.center,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: currentStyle,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
129
pubspec.lock
129
pubspec.lock
|
|
@ -302,6 +302,15 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.6.0"
|
||||
flutter_highlight:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
path: flutter_highlight
|
||||
ref: HEAD
|
||||
resolved-ref: "5179638b4c3a05528e1860f50ea64a95f393f2be"
|
||||
url: "https://github.com/JHubi1/highlight.dart"
|
||||
source: git
|
||||
version: "0.7.0"
|
||||
flutter_install_referrer:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -339,14 +348,14 @@ packages:
|
|||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
flutter_markdown:
|
||||
flutter_math_fork:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_markdown
|
||||
sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27"
|
||||
name: flutter_math_fork
|
||||
sha256: "6d5f2f1aa57ae539ffb0a04bb39d2da67af74601d685a161aff7ce5bda5fa407"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.7+1"
|
||||
version: "0.7.4"
|
||||
flutter_parsed_text:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -363,6 +372,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.29"
|
||||
flutter_svg:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_svg
|
||||
sha256: cd57f7969b4679317c17af6fd16ee233c1e60a82ed209d8a475c54fd6fd6f845
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
|
@ -389,6 +406,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.4.4"
|
||||
highlight:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: highlight
|
||||
sha256: "5353a83ffe3e3eca7df0abfb72dcf3fa66cc56b953728e7113ad4ad88497cf21"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
html:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -413,14 +438,6 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.2"
|
||||
hyphenatorx:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: hyphenatorx
|
||||
sha256: "9afe950d84e56fde91b9129bf3c95e86e70f19a20e24ba6458ee1d028a64816e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.5.11"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -589,6 +606,22 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.0"
|
||||
nested:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: nested
|
||||
sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
ogp_data_extract:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: ogp_data_extract
|
||||
sha256: "6389120f7d0a20d9bff5c37c5b46d8977aea2e604f5a04ff75fee597b8865269"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.2"
|
||||
ollama_dart:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -621,6 +654,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
path_parsing:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_parsing
|
||||
sha256: "883402936929eac138ee0a45da5b0f2c80f89913e6dc3bf77eb65b84b409c6ca"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -701,6 +742,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: petitparser
|
||||
sha256: "1a97266a94f7350d30ae522c0af07890c70b8e62c71e8e3920d1db4d23c057d1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
photo_view:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -725,6 +774,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
provider:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: provider
|
||||
sha256: "4e82183fa20e5ca25703ead7e05de9e4cceed1fbd1eadc1ac3cb6f565a09f272"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.1.5+1"
|
||||
pwa_install:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
|
|
@ -882,6 +939,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.4.1"
|
||||
string_validator:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: string_validator
|
||||
sha256: "240f4c98027dfbe8639c8271ef18cc9de735b47067aa15a720cfed9576a512b1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.2.0"
|
||||
term_glyph:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -914,6 +979,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.1"
|
||||
tuple:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: tuple
|
||||
sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1010,6 +1083,30 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.5.1"
|
||||
vector_graphics:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics
|
||||
sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.19"
|
||||
vector_graphics_codec:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_codec
|
||||
sha256: "99fd9fbd34d9f9a32efd7b6a6aae14125d8237b10403b422a6a6dfeac2806146"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.13"
|
||||
vector_graphics_compiler:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: vector_graphics_compiler
|
||||
sha256: d354a7ec6931e6047785f4db12a1f61ec3d43b207fc0790f863818543f8ff0dc
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.19"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
|
@ -1066,6 +1163,14 @@ packages:
|
|||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xml
|
||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "6.6.1"
|
||||
sdks:
|
||||
dart: ">=3.8.1 <4.0.0"
|
||||
flutter: ">=3.35.0"
|
||||
|
|
|
|||
10
pubspec.yaml
10
pubspec.yaml
|
|
@ -26,7 +26,6 @@ dependencies:
|
|||
transparent_image: ^2.0.1
|
||||
simple_icons: ^14.6.1
|
||||
url_launcher: ^6.2.6
|
||||
flutter_markdown: ^0.7.1
|
||||
file_picker: ^8.0.3
|
||||
file_selector: ^1.0.3
|
||||
bitsdojo_window: ^0.1.6
|
||||
|
|
@ -42,8 +41,13 @@ dependencies:
|
|||
universal_html: ^2.2.4
|
||||
pwa_install: ^0.0.5
|
||||
flutter_chat_types: any
|
||||
markdown: any
|
||||
hyphenatorx: ^1.5.11
|
||||
markdown: ^7.3.0
|
||||
flutter_math_fork: ^0.7.4
|
||||
flutter_highlight:
|
||||
git:
|
||||
url: https://github.com/JHubi1/highlight.dart
|
||||
path: flutter_highlight
|
||||
ogp_data_extract: ^0.2.2
|
||||
|
||||
# dynamic_color: ^1.7.0
|
||||
dynamic_system_colors: ^1.8.0
|
||||
|
|
|
|||
Loading…
Reference in New Issue