From f8a37ed80d074c4cf639076b5b3525d2b280f44d Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Tue, 20 Aug 2024 15:52:57 +0200 Subject: [PATCH 01/13] Rename formatters.dart to format.dart and the Formatters class to FormatHelpers --- app/lib/core/{formatters.dart => format.dart} | 2 +- app/lib/core/logger.dart | 4 ++-- app/lib/frontend/pages/dashboard/models.dart | 4 ++-- app/lib/frontend/pages/dashboard/sessions.dart | 5 ++--- app/lib/frontend/widgets/chat_message.dart | 4 ++-- 5 files changed, 9 insertions(+), 10 deletions(-) rename app/lib/core/{formatters.dart => format.dart} (86%) diff --git a/app/lib/core/formatters.dart b/app/lib/core/format.dart similarity index 86% rename from app/lib/core/formatters.dart rename to app/lib/core/format.dart index 6a6ba07..acba91b 100644 --- a/app/lib/core/formatters.dart +++ b/app/lib/core/format.dart @@ -1,6 +1,6 @@ import 'package:intl/intl.dart'; -class Fortmatters { +class FortmatHelpers { static String standardDate(DateTime dateTime) { return DateFormat("dd/MM/yyyy HH:mm:ss").format(dateTime); } diff --git a/app/lib/core/logger.dart b/app/lib/core/logger.dart index 4d5598f..96e0172 100644 --- a/app/lib/core/logger.dart +++ b/app/lib/core/logger.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; -import 'package:open_local_ui/core/formatters.dart'; +import 'package:open_local_ui/core/format.dart'; import 'package:path_provider/path_provider.dart'; late Logger logger; @@ -47,7 +47,7 @@ Future initLogger() async { } Future createLogFile() async { - final timeStamp = Fortmatters.standardDate(DateTime.now()) + final timeStamp = FortmatHelpers.standardDate(DateTime.now()) .replaceAll(' ', '_') .replaceAll('/', '-') .replaceAll(':', '-'); diff --git a/app/lib/frontend/pages/dashboard/models.dart b/app/lib/frontend/pages/dashboard/models.dart index b7a1eaf..ec1a15a 100644 --- a/app/lib/frontend/pages/dashboard/models.dart +++ b/app/lib/frontend/pages/dashboard/models.dart @@ -7,7 +7,7 @@ import 'package:open_local_ui/backend/models/model.dart'; import 'package:open_local_ui/backend/providers/chat.dart'; import 'package:open_local_ui/backend/providers/model.dart'; import 'package:open_local_ui/core/asset.dart'; -import 'package:open_local_ui/core/formatters.dart'; +import 'package:open_local_ui/core/format.dart'; import 'package:open_local_ui/frontend/dialogs/confirmation.dart'; import 'package:open_local_ui/frontend/dialogs/create_model.dart'; import 'package:open_local_ui/frontend/dialogs/import_model.dart'; @@ -451,7 +451,7 @@ class _ModelListTileState extends State { title: Text(widget.model.name), subtitle: Text( AppLocalizations.of(context).modifiedAtTextShared( - Fortmatters.standardDate(widget.model.modifiedAt), + FortmatHelpers.standardDate(widget.model.modifiedAt), ), ), trailing: Row( diff --git a/app/lib/frontend/pages/dashboard/sessions.dart b/app/lib/frontend/pages/dashboard/sessions.dart index ba734a9..93404ba 100644 --- a/app/lib/frontend/pages/dashboard/sessions.dart +++ b/app/lib/frontend/pages/dashboard/sessions.dart @@ -2,13 +2,12 @@ import 'dart:io'; import 'package:flutter/material.dart'; - import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:gap/gap.dart'; import 'package:open_local_ui/backend/models/chat_session.dart'; import 'package:open_local_ui/backend/providers/chat.dart'; -import 'package:open_local_ui/core/formatters.dart'; +import 'package:open_local_ui/core/format.dart'; import 'package:open_local_ui/frontend/dialogs/confirmation.dart'; import 'package:open_local_ui/frontend/helpers/snackbar.dart'; import 'package:open_local_ui/frontend/screens/dashboard.dart'; @@ -409,7 +408,7 @@ class _SessionListTileState extends State { ? null : Text( AppLocalizations.of(context).createdAtTextShared( - Fortmatters.standardDate(widget.session.createdAt), + FortmatHelpers.standardDate(widget.session.createdAt), ), ), trailing: Row( diff --git a/app/lib/frontend/widgets/chat_message.dart b/app/lib/frontend/widgets/chat_message.dart index 581fdf5..6a4a6b1 100644 --- a/app/lib/frontend/widgets/chat_message.dart +++ b/app/lib/frontend/widgets/chat_message.dart @@ -11,7 +11,7 @@ import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:gap/gap.dart'; import 'package:open_local_ui/backend/models/chat_message.dart'; import 'package:open_local_ui/backend/providers/chat.dart'; -import 'package:open_local_ui/core/formatters.dart'; +import 'package:open_local_ui/core/format.dart'; import 'package:open_local_ui/frontend/helpers/snackbar.dart'; import 'package:open_local_ui/frontend/widgets/markdown_body.dart'; import 'package:open_local_ui/frontend/widgets/tts_player.dart'; @@ -198,7 +198,7 @@ class _ChatMessageWidgetState extends State { ), const Gap(8), Text( - Fortmatters.standardDate(widget.message.createdAt), + FortmatHelpers.standardDate(widget.message.createdAt), style: const TextStyle( fontSize: 18.0, fontWeight: FontWeight.w100, From d6267228a006caec24eb60bc99e32e712a709b3e Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Wed, 21 Aug 2024 12:15:56 +0200 Subject: [PATCH 02/13] Rename HTTPMethods class to HTTPHelpers --- app/lib/backend/providers/model.dart | 11 +++++------ app/lib/core/http.dart | 2 +- app/lib/frontend/dialogs/attachments_dropzone.dart | 2 +- app/lib/frontend/dialogs/pull_model.dart | 2 +- app/lib/frontend/dialogs/push_model.dart | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/app/lib/backend/providers/model.dart b/app/lib/backend/providers/model.dart index 614b52d..e3f8e5c 100644 --- a/app/lib/backend/providers/model.dart +++ b/app/lib/backend/providers/model.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; - import 'package:http/http.dart' as http; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:open_local_ui/backend/models/model.dart'; @@ -62,7 +61,7 @@ class ModelProvider extends ChangeNotifier { static Future _isOlamaRunning() async { try { - final response = await HTTPMethods.get('$_api/ps'); + final response = await HTTPHelpers.get('$_api/ps'); return response.statusCode == HttpStatus.ok; } catch (e) { return false; @@ -74,7 +73,7 @@ class ModelProvider extends ChangeNotifier { } static Future _updateListStatic() async { - await HTTPMethods.get('$_api/tags').then((response) { + await HTTPHelpers.get('$_api/tags').then((response) { if (response.statusCode != 200) { logger.e('Failed to fetch models list'); return; @@ -182,7 +181,7 @@ class ModelProvider extends ChangeNotifier { WindowsTaskbar.resetThumbnailToolbar(); WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); } - + SnackBarHelpers.showSnackBar( // ignore: use_build_context_synchronously AppLocalizations.of(scaffoldMessengerKey.currentState!.context) @@ -393,7 +392,7 @@ class ModelProvider extends ChangeNotifier { WindowsTaskbar.resetThumbnailToolbar(); WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); } - + SnackBarHelpers.showSnackBar( // ignore: use_build_context_synchronously AppLocalizations.of(scaffoldMessengerKey.currentState!.context) @@ -421,7 +420,7 @@ class ModelProvider extends ChangeNotifier { Future remove(String name) async { try { final response = - await HTTPMethods.delete('$_api/delete', body: {'name': name}); + await HTTPHelpers.delete('$_api/delete', body: {'name': name}); if (response.statusCode != 200) { logger.e( 'Failed to remove model $name, status code: ${response.statusCode}'); diff --git a/app/lib/core/http.dart b/app/lib/core/http.dart index d980583..9963cba 100644 --- a/app/lib/core/http.dart +++ b/app/lib/core/http.dart @@ -42,7 +42,7 @@ class HTTPStreamResponse extends HTTPResponse { Map toJson() => _$HTTPStreamResponseToJson(this); } -class HTTPMethods { +class HTTPHelpers { static Future get(String url) async { return http.get(Uri.parse(url)); } diff --git a/app/lib/frontend/dialogs/attachments_dropzone.dart b/app/lib/frontend/dialogs/attachments_dropzone.dart index 22cbae9..9c3259f 100644 --- a/app/lib/frontend/dialogs/attachments_dropzone.dart +++ b/app/lib/frontend/dialogs/attachments_dropzone.dart @@ -108,7 +108,7 @@ class _AttachmentsDropzoneDialogState extends State { url = _extractUrlFromQuery(text) ?? text; } - final response = await HTTPMethods.get(url); + final response = await HTTPHelpers.get(url); if (response.statusCode != 200) { setState(() { diff --git a/app/lib/frontend/dialogs/pull_model.dart b/app/lib/frontend/dialogs/pull_model.dart index 1db4e29..c2c76db 100644 --- a/app/lib/frontend/dialogs/pull_model.dart +++ b/app/lib/frontend/dialogs/pull_model.dart @@ -74,7 +74,7 @@ class _PullModelDialogState extends State { final total = response.total; final completed = response.completed; final progressValue = completed / total; - final duration = HTTPMethods.calculateRemainingTime(response); + final duration = HTTPHelpers.calculateRemainingTime(response); final fmt = NumberFormat('#00'); final progressBarText = AppLocalizations.of(context) .progressBarStatusWithTimeText( diff --git a/app/lib/frontend/dialogs/push_model.dart b/app/lib/frontend/dialogs/push_model.dart index 67a95b7..12487ad 100644 --- a/app/lib/frontend/dialogs/push_model.dart +++ b/app/lib/frontend/dialogs/push_model.dart @@ -98,7 +98,7 @@ class _PushModelDialogState extends State { final total = response.total; final completed = response.completed; final progressValue = completed / total; - final duration = HTTPMethods.calculateRemainingTime(response); + final duration = HTTPHelpers.calculateRemainingTime(response); final progressBarText = AppLocalizations.of(context) .progressBarStatusWithTimeText( response.status, From 8a2303e14c69ebe07638c067ca4bfed14b28fc9a Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Wed, 21 Aug 2024 21:46:31 +0200 Subject: [PATCH 03/13] Fully document backend and core --- app/lib/backend/databases/chat_sessions.dart | 10 + app/lib/backend/models/chat_message.dart | 41 ++- app/lib/backend/models/chat_session.dart | 20 +- app/lib/backend/models/model.dart | 10 + app/lib/backend/models/ollama_responses.dart | 12 + app/lib/backend/providers/chat.dart | 269 ++++++++++++++---- app/lib/backend/providers/locale.dart | 6 + app/lib/backend/providers/model.dart | 73 ++++- app/lib/backend/providers/model_settings.dart | 86 ++++-- app/lib/backend/services/tts.dart | 5 + app/lib/components/rive_animation.dart | 5 +- app/lib/core/asset.dart | 153 ++++++++-- app/lib/core/color.dart | 12 + app/lib/core/format.dart | 4 + app/lib/core/github.dart | 65 ++++- app/lib/core/http.dart | 54 +++- app/lib/core/image.dart | 11 + app/lib/core/logger.dart | 16 +- app/lib/core/process.dart | 9 + app/lib/core/update.dart | 74 +++-- app/lib/frontend/pages/dashboard/about.dart | 5 +- app/lib/frontend/pages/dashboard/models.dart | 2 +- .../frontend/widgets/chat_input_field.dart | 1 - .../widgets/markdown_code_wrapper.dart | 3 +- app/lib/main.dart | 166 +++++++---- 25 files changed, 912 insertions(+), 200 deletions(-) diff --git a/app/lib/backend/databases/chat_sessions.dart b/app/lib/backend/databases/chat_sessions.dart index 4e9f22e..15885d3 100644 --- a/app/lib/backend/databases/chat_sessions.dart +++ b/app/lib/backend/databases/chat_sessions.dart @@ -4,12 +4,22 @@ import 'package:hive_flutter/hive_flutter.dart'; import 'package:open_local_ui/backend/models/chat_session.dart'; import 'package:path_provider/path_provider.dart'; +/// This class provides methods for saving, updating, deleting, and loading chat sessions. +/// +/// The chat sessions are stored in a Hive database. This allows to easily save and load chat sessions in the JSON format. +/// +/// The Hive Box used to store the chat sessions is named 'sessions'. +/// You can find the database files in the support directory of the app (see the output of [getApplicationSupportDirectory]). class ChatSessionsDatabase { + /// Initializes the chat sessions database. + /// + /// NOTE: This method must be called before any other methods in this class are called inside the current isolate. static Future init() async { final dataDir = await getApplicationSupportDirectory(); Hive.init('${dataDir.path}/sessions'); } + /// Deinitializes the chat sessions database. static Future deinit() async { await Hive.close(); } diff --git a/app/lib/backend/models/chat_message.dart b/app/lib/backend/models/chat_message.dart index 675c4a7..8b831cd 100644 --- a/app/lib/backend/models/chat_message.dart +++ b/app/lib/backend/models/chat_message.dart @@ -5,6 +5,9 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'chat_message.g.dart'; +/// Converts [Uint8List] to [Object] and vice versa for JSON serialization. +/// +/// This class is used as a JSON converter for the [ChatUserMessageWrapper.imageBytes] property. class ImageBytesJSONConverter implements JsonConverter { const ImageBytesJSONConverter(); @@ -31,6 +34,9 @@ class ImageBytesJSONConverter implements JsonConverter { enum ChatMessageSender { user, model, system } +/// Converts the [ChatMessageSender] object to JSON and vice versa. +/// +/// This class is used as a JSON converter for the [ChatMessageWrapper.sender] property. class ChatMessageSenderJSONConverter implements JsonConverter { const ChatMessageSenderJSONConverter(); @@ -62,7 +68,29 @@ class ChatMessageSenderJSONConverter } } -// NOTE: named with 'Wrapper' suffix to avoid conflict with LangChain +/// NOTE: named with 'Wrapper' suffix to avoid conflict with langchain.dart +/// +/// This class is used to encapsulate the properties of a chat message. +/// +/// The [ChatMessageWrapper] class is annotated with `@JsonSerializable` to enable JSON serialization and deserialization. +/// +/// Properties: +/// - `text`: The text content of the chat message. +/// - `createdAt`: The date and time when the chat message was created. +/// - `uuid`: The unique identifier of the chat message. +/// - `senderName`: The name of the sender of the chat message (optional). +/// - `sender`: The sender of the chat message. +/// +/// For metadata and usage statistics see [ChatResult.metadata] in langchain.dart. +/// - `totalDuration`: The total duration of the chat message. +/// - `loadDuration`: The duration it took to load the chat message. +/// - `promptEvalCount`: The number of prompt evaluations performed on the chat message. +/// - `promptEvalDuration`: The duration of prompt evaluations performed on the chat message. +/// - `evalCount`: The number of evaluations performed on the chat message. +/// - `evalDuration`: The duration of evaluations performed on the chat message. +/// - `promptTokens`: The number of prompt tokens in the chat message. +/// - `responseTokens`: The number of response tokens in the chat message. +/// - `totalTokens`: The total number of tokens in the chat message. @JsonSerializable() class ChatMessageWrapper { String text; @@ -120,6 +148,9 @@ class ChatMessageWrapper { const ChatMessageSenderJSONConverter().toJson(object); } +/// Represents a system message in the chat. +/// +/// This class extends the [ChatMessageWrapper] class and sets the sender name and type to 'System'. @JsonSerializable() class ChatSystemMessageWrapper extends ChatMessageWrapper { ChatSystemMessageWrapper( @@ -141,6 +172,9 @@ class ChatSystemMessageWrapper extends ChatMessageWrapper { Map toJson() => _$ChatSystemMessageWrapperToJson(this); } +/// Represents a model message in the chat. +/// +/// This class extends the [ChatMessageWrapper] class and sets the sender name and type to 'Model'. @JsonSerializable() class ChatModelMessageWrapper extends ChatMessageWrapper { ChatModelMessageWrapper( @@ -163,6 +197,11 @@ class ChatModelMessageWrapper extends ChatMessageWrapper { Map toJson() => _$ChatModelMessageWrapperToJson(this); } +/// Represents a user message in the chat. +/// +/// This class extends the [ChatMessageWrapper] class and sets the sender name and type to 'User'. +/// +/// The [ChatUserMessageWrapper] class also includes an optional [imageBytes] property to store image data for use with multimodal models. @JsonSerializable() class ChatUserMessageWrapper extends ChatMessageWrapper { @JsonKey( diff --git a/app/lib/backend/models/chat_session.dart b/app/lib/backend/models/chat_session.dart index e5ad2d7..974ec18 100644 --- a/app/lib/backend/models/chat_session.dart +++ b/app/lib/backend/models/chat_session.dart @@ -4,6 +4,9 @@ import 'package:open_local_ui/backend/models/chat_message.dart'; part 'chat_session.g.dart'; +/// Converts JSON data to [ChatMessageWrapper] object and vice versa. +/// +/// This class determines the type of [ChatMessageWrapper] object to be created based on the 'sender' property. class ChatMessagesJSONConverter implements JsonConverter, List>> { @@ -37,7 +40,22 @@ enum ChatSessionStatus { aborting, } -// NOTE: named with 'Wrapper' suffix to avoid conflict with LangChain +/// NOTE: named with 'Wrapper' suffix to avoid conflict with lancghain.dart +/// +/// This class is used to encapsulate the properties of a chat session. +/// +/// The [ChatSessionWrapper] class is annotated with `@JsonSerializable` to enable JSON serialization and deserialization. +/// +/// Properties: +/// - `title`: The title of the chat session. +/// - `createdAt`: The date and time when the chat session was created. +/// - `uuid`: The unique identifier of the chat session. +/// - `messages`: The list of chat messages associated with the chat session. +/// - `status`: The status of the chat session. +/// +/// The [ChatSessionWrapper] class also contains a [memory] property of type [ConversationBufferMemory] for use by langchain.dart. +/// +/// NOTE: In the future messages will be stored in an N-Ary tree structure to allow for branching conversations. @JsonSerializable() class ChatSessionWrapper { String title; diff --git a/app/lib/backend/models/model.dart b/app/lib/backend/models/model.dart index ab50015..b422678 100644 --- a/app/lib/backend/models/model.dart +++ b/app/lib/backend/models/model.dart @@ -2,6 +2,13 @@ import 'package:freezed_annotation/freezed_annotation.dart'; part 'model.g.dart'; +/// This class is used to encapsulate the properties of an Ollama model. +/// +/// The [Model] class is annotated with `@JsonSerializable` to enable JSON serialization and deserialization. +/// +/// NOTE: The casing of the fields in the JSON data is forced to snake_case for interoperability with the Ollama REST API. This allows Ollama API responses to be converted to [Model] objects. +/// +/// Model options are encapsulated in the [ModelSettings] class. @JsonSerializable(fieldRename: FieldRename.snake) class Model { final String name; @@ -45,6 +52,9 @@ class ModelDetails { Map toJson() => _$ModelDetailsToJson(this); } +/// This class is used to encapsulate the properties of the Ollama model settings. +/// +/// For more information on the model settings, refer to the Ollama API documentation at https://github.com/ollama/ollama/blob/main/docs/modelfile.md#valid-parameters-and-values. @JsonSerializable() class ModelSettings { String? systemPrompt; diff --git a/app/lib/backend/models/ollama_responses.dart b/app/lib/backend/models/ollama_responses.dart index 19af5fa..eba16f9 100644 --- a/app/lib/backend/models/ollama_responses.dart +++ b/app/lib/backend/models/ollama_responses.dart @@ -3,6 +3,10 @@ import 'package:open_local_ui/core/http.dart'; part 'ollama_responses.g.dart'; +/// Represents a response from the Ollama API when pulling a model from registry. +/// +/// This class extends the [HTTPStreamResponse] class and provides additional functionality specific to Ollama responses. + @JsonSerializable() class OllamaPullResponse extends HTTPStreamResponse { OllamaPullResponse({ @@ -20,6 +24,11 @@ class OllamaPullResponse extends HTTPStreamResponse { Map toJson() => _$OllamaPullResponseToJson(this); } +@JsonSerializable() + +/// Represents a response from the Ollama API when pushing a model to the registry. +/// +/// This class extends the [HTTPStreamResponse] class and provides additional functionality specific to OllamaPush responses. @JsonSerializable() class OllamaPushResponse extends HTTPStreamResponse { OllamaPushResponse({ @@ -37,6 +46,9 @@ class OllamaPushResponse extends HTTPStreamResponse { Map toJson() => _$OllamaPushResponseToJson(this); } +/// Represents the response returned from the Ollama API when creating a new model locally. +/// +/// This class extends the [HTTPStreamResponse] class and provides additional functionality specific to OllamaPush responses. @JsonSerializable() class OllamaCreateResponse extends HTTPStreamResponse { OllamaCreateResponse({ diff --git a/app/lib/backend/providers/chat.dart b/app/lib/backend/providers/chat.dart index 7d4db5f..dc5554b 100644 --- a/app/lib/backend/providers/chat.dart +++ b/app/lib/backend/providers/chat.dart @@ -22,10 +22,21 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:uuid/uuid.dart'; import 'package:windows_taskbar/windows_taskbar.dart'; +/// A provider class for managing chats in all their aspects. +/// +/// This class extends the [ChangeNotifier] class, allowing it to notify listeners when the chat state changes. +/// +/// The chat provider is responsible for: +/// - Managing chat sessions +/// - Managing chat messages +/// - Handling chat logic +/// +/// This class wraps around the langchain.dart library, which provides us with models, agents, databases and other tools. class ChatProvider extends ChangeNotifier { // Langchain objects ChatOllama _chat; - // Model settings + + // Global override settings String _modelName; bool _enableGPU; double _temperature; @@ -33,6 +44,8 @@ class ChatProvider extends ChangeNotifier { bool _enableWebSearch; bool _enableDocsSearch; bool _showStatistics; + + // Model specific settings late ModelSettings _modelSettings; // Chat session @@ -53,6 +66,13 @@ class ChatProvider extends ChangeNotifier { loadSettings(); } + /// Called when the provider is initialized to load model specific, global override settings and chat sessions. + /// + /// Global override settings are stored using the [SharedPreferences] plugin. + /// Model specific settings are stored in the app's data directory as JSON files (see [ModelSettingsProvider]). + /// Chat sessions are stored in the app's data directory using the Hive database (see [ChatSessionsDatabase]). + /// + /// Returns a [Future] that evaluates to `void`. void loadSettings() async { final prefs = await SharedPreferences.getInstance(); @@ -117,8 +137,13 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } - // Sessions management + /////////////////////////////////////////// + // Sessions management // + /////////////////////////////////////////// + /// Adds a new chat session with the given title and saves it to the database. + /// + /// Returns the newly created [ChatSessionWrapper]. ChatSessionWrapper addSession(String title) { _sessions.add(ChatSessionWrapper( DateTime.now(), @@ -133,12 +158,20 @@ class ChatProvider extends ChangeNotifier { return _sessions.last; } + /// Creates a new chat session and sets it as the current session. + /// + /// Returns `void`. void newSession() { final session = addSession(''); setSession(session.uuid); notifyListeners(); } + /// Sets the current session to the one with the given UUID, loads its chat history and sets the window title. + /// + /// If the acrive session is currently generating, the function prevents the session from being changed. + /// + /// Returns `void`. void setSession(String uuid) { if (isGenerating) return; @@ -175,6 +208,11 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } + /// Removes the session with the given UUID from the list of sessions and deletes it from the database. + /// + /// If the session is active and currently generating, the function prevents the session from being removed. + /// + /// Returns `void`. void removeSession(String uuid) { final index = _sessions.indexWhere((element) => element.uuid == uuid); @@ -197,6 +235,12 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } + /// Removes all sessions from the list of sessions and deletes them from the database. + /// + /// Under the hood, the function iterates over the list of sessions and removes each session one by one. + /// This means it follows the same logic as [removeSession] inherits its constraints. + /// + /// Returns `void`. void clearSessions() { List uuids = []; @@ -209,6 +253,9 @@ class ChatProvider extends ChangeNotifier { } } + /// Sets the title of the session with the given UUID to the given title, updates the session in the database and updates the window title if the session is currently active. + /// + /// Returns `void`. void setSessionTitle(String uuid, String title) { final index = _sessions.indexWhere((element) => element.uuid == uuid); @@ -227,8 +274,15 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } - // Messages management + /////////////////////////////////////////// + // Messages management // + /////////////////////////////////////////// + /// Adds a chat message of type system to the current session and to the model's memory and updates the session in the database. + /// + /// If the session is not selected, the function returns the newly created [ChatSystemMessageWrapper] without adding it to the memory or the database. + /// + /// Returns the newly created [ChatSystemMessageWrapper]. ChatSystemMessageWrapper addSystemMessage(String message) { final chatMessage = ChatSystemMessageWrapper( message, @@ -240,6 +294,8 @@ class ChatProvider extends ChangeNotifier { _session!.messages.add(chatMessage); + // System messages shouldn't be added to the memory + ChatSessionsDatabase.updateSession(_session!); notifyListeners(); @@ -247,6 +303,11 @@ class ChatProvider extends ChangeNotifier { return _session!.messages.last as ChatSystemMessageWrapper; } + /// Adds a chat message of type model to the current session and to the model's memory and updates the session in the database. + /// + /// If the session is not selected, the function returns the newly created [ChatModelMessageWrapper] without adding it to the memory or the database. + /// + /// Returns the newly created [ChatModelMessageWrapper]. ChatModelMessageWrapper addModelMessage(String message, String senderName) { final chatMessage = ChatModelMessageWrapper( message, @@ -258,6 +319,7 @@ class ChatProvider extends ChangeNotifier { if (!isSessionSelected) return chatMessage; _session!.messages.add(chatMessage); + _session!.memory.chatHistory.addAIChatMessage(message); ChatSessionsDatabase.updateSession(_session!); @@ -266,6 +328,13 @@ class ChatProvider extends ChangeNotifier { return _session!.messages.last as ChatModelMessageWrapper; } + /// Adds a chat message of type user to the current session and to the model's memory and updates the session in the database. + /// + /// If the session is not selected, the function returns the newly created [ChatUserMessageWrapper] without adding it to the memory or the database. + /// + /// User messages have optional [imageBytes] attached to them for use in multimodal models. + /// + /// Returns the newly created [ChatUserMessageWrapper]. ChatMessageWrapper addUserMessage(String message, Uint8List? imageBytes) { final chatMessage = ChatUserMessageWrapper( message, @@ -277,6 +346,7 @@ class ChatProvider extends ChangeNotifier { if (_session == null) return chatMessage; _session!.messages.add(chatMessage); + _session!.memory.chatHistory.addHumanChatMessage(message); ChatSessionsDatabase.updateSession(_session!); @@ -285,6 +355,11 @@ class ChatProvider extends ChangeNotifier { return _session!.messages.last; } + /// Removes the the message with the given UUID and its childs from the current session and from the model's memory and updates the session in the database. + /// + /// If the session is active and currently generating, the function prevents the messages from being removed. + /// + /// Returns `void`. void removeMessage(String uuid) async { if (!isSessionSelected || isGenerating) return; @@ -292,22 +367,6 @@ class ChatProvider extends ChangeNotifier { (element) => element.uuid == uuid, ); - _session!.messages.removeAt(index); - - _session!.memory.chatHistory.removeLast(); - - ChatSessionsDatabase.updateSession(_session!); - - notifyListeners(); - } - - void removeFromMessage(String uuid) async { - if (!isSessionSelected || isGenerating) return; - - final index = _session!.messages.indexWhere( - (element) => element.uuid == uuid, - ); - _session!.messages.removeRange(index, messageCount); for (var i = 0; i < messageCount - index; ++i) { @@ -319,6 +378,11 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } + /// Removes the last message from the current session and from the model's memory and updates the session in the database. + /// + /// If the session is active and currently generating, the function prevents the message from being removed. + /// + /// Returns `void`. void removeLastMessage() async { if (!isSessionSelected || isGenerating || messageCount == 0) { return; @@ -343,8 +407,11 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } - // Chat logic + /////////////////////////////////////////// + // Chat logic management // + /////////////////////////////////////////// + /// Builds a chat chain that processes the user's input and generates a response. Future _buildChain() async { final systemPrompt = await _loadSystemPrompt(); @@ -369,8 +436,7 @@ class ChatProvider extends ChangeNotifier { return chain; } - /// Use model specific prompt if available, otherwise use default - /// from assets + /// Use model specific prompt if available, otherwise use default from assets. Future _loadSystemPrompt() async { if (_modelSettings.systemPrompt != null && _modelSettings.systemPrompt!.isNotEmpty) { @@ -380,6 +446,7 @@ class ChatProvider extends ChangeNotifier { return rootBundle.loadString('assets/prompts/default.txt'); } + /// Builds a prompt message with the given text and optional image bytes. ChatMessage _buildPrompt(String text, {Uint8List? imageBytes}) { final prompt = ChatMessage.human( ChatMessageContent.multiModal( @@ -398,6 +465,18 @@ class ChatProvider extends ChangeNotifier { return prompt; } + /// Sends a message to the chat model and processes the response. + /// + /// The function first checks if a session is selected and creates a new one if not. + /// If the text is empty, the function sends a system message and aborts generation. + /// If no model is selected, the function sends a system message and aborts generation. + /// The function then sets the session status to generating. + /// It first add the user and the mepty model messages to the session. + /// It then builds a chat chain and a prompt message to start generating the response. + /// The response is streamed to offer real-time updates to the user. + /// If the sessions is untitled, the function generates a title for it. + /// + /// Returns a [Future] that evaluates to `null`. Future sendMessage(String text, {Uint8List? imageBytes}) async { if (!isSessionSelected) { newSession(); @@ -405,12 +484,11 @@ class ChatProvider extends ChangeNotifier { if (text.isEmpty) { addSystemMessage('Try to be more specific.'); - return; } + if (!isModelSelected) { addSystemMessage('Please select a model.'); - return; } @@ -426,10 +504,6 @@ class ChatProvider extends ChangeNotifier { addUserMessage(text, imageBytes); - _session!.memory.chatHistory.addHumanChatMessage( - _session!.messages.last.text, - ); - final chain = await _buildChain(); final prompt = _buildPrompt(text, imageBytes: imageBytes); @@ -439,6 +513,8 @@ class ChatProvider extends ChangeNotifier { await for (final response in chain.stream([prompt])) { ChatResult result = response as ChatResult; + // If the session is aborted, remove the last message from memory and break the loop + if (_session!.status == ChatSessionStatus.aborting) { _session!.status = ChatSessionStatus.idle; _session!.memory.chatHistory.removeLast(); @@ -458,9 +534,13 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } - _session!.memory.chatHistory.addAIChatMessage( - _session!.messages.last.text, - ); + // Save the generated message, remove and add it back to force a memory update + + final generatedText = _session!.messages.last.text; + + removeLastMessage(); + + addModelMessage(generatedText, _modelName); _session!.status = ChatSessionStatus.idle; @@ -471,6 +551,8 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); + // If the session is untitled, generate a title + if (_session!.title == 'Untitled') { final titleGeneratorPrompt = await rootBundle.loadString( 'assets/prompts/sessions_title_generator.txt', @@ -498,6 +580,8 @@ class ChatProvider extends ChangeNotifier { removeLastMessage(); + // Add a system message to inform the user about the error + addSystemMessage('An error occurred while generating the response.'); notifyListeners(); @@ -508,8 +592,17 @@ class ChatProvider extends ChangeNotifier { } } - void regenerateMessage(String uuid) async { - if (!isSessionSelected || isGenerating) return; + /// Regenerates the message with the given UUID for the last user message. + /// + /// If the sessions is currently generating, the function returns without doing anything. + /// NOTE: We don't need to check if the session is selected because the function is only called from the UI. + /// The function first removes the last generated message from the session. + /// Then it finds the last user message before the model message and uses its text to regenerate the response. + /// This method does not regenerate the title of the session as it depends on the user's input. + /// + /// Returns a [Future] that evaluates to `null`. + Future regenerateMessage(String uuid) async { + if (isGenerating) return; final modelMessageIndex = _session!.messages.indexWhere( (element) => element.uuid == uuid, @@ -519,7 +612,7 @@ class ChatProvider extends ChangeNotifier { return; } - removeFromMessage(uuid); + removeMessage(uuid); final userMessageIndex = _session!.messages.lastIndexWhere( (element) => element is ChatUserMessageWrapper, @@ -576,9 +669,11 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } - _session!.memory.chatHistory.addAIChatMessage( - _session!.messages.last.text, - ); + final generatedText = _session!.messages.last.text; + + removeLastMessage(); + + addModelMessage(generatedText, _modelName); _session!.status = ChatSessionStatus.idle; @@ -606,12 +701,20 @@ class ChatProvider extends ChangeNotifier { } } - void sendEditedMessage( + /// Regenerates the last message with the edited text and image bytes from the user. + /// + /// If the sessions is currently generating, the function returns without doing anything. + /// NOTE: We don't need to check if the session is selected because the function is only called from the UI. + /// The function first removes the last interaction from the session. + /// Then it sends the edited message to the model for regeneration. + /// + /// Returns a [Future] that evaluates to `null`. + Future sendEditedMessage( String uuid, String text, Uint8List? imageBytes, ) async { - if (!isSessionSelected || isGenerating) return; + if (isGenerating) return; final messageIndex = _session!.messages.indexWhere( (element) => element.uuid == uuid, @@ -621,11 +724,16 @@ class ChatProvider extends ChangeNotifier { return; } - removeFromMessage(uuid); + removeMessage(uuid); sendMessage(text, imageBytes: imageBytes); } + /// Aborts the current session's generation process. + /// + /// If the session is not selected or currently generating, the function returns without doing anything. + /// + /// Returns `void`. void abortGeneration() { if (!isSessionSelected || !isGenerating) return; @@ -634,8 +742,16 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } - // Session history management + /////////////////////////////////////////// + // Session history management // + /////////////////////////////////////////// + /// Loads the chat history of the current session. + /// The function iterates over the messages of the session and adds them to the model's memory based on their sender. + /// + /// If the session is not selected or currently generating, the function returns without doing anything. + /// + /// Returns `void`. void loadSessionHistory() async { if (!isSessionSelected || isGenerating) return; @@ -659,6 +775,11 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } + /// Wipes the chat history of the current session. + /// + /// If the session is not selected or currently generating, the function returns without doing anything. + /// + /// Returns `void`. void clearSessionHistory() async { if (!isSessionSelected || isGenerating) return; @@ -667,8 +788,30 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } - // Model configuration + /////////////////////////////////////////// + // Settings management // + /////////////////////////////////////////// + + /// Sets the current model to the one with the given name and loads its settings + /// + /// The function first checks if the model is currently generating and returns without doing anything if it is. + /// + /// Returns a [Future] that evaluates to `void`. + Future setModel(String name) async { + if (isGenerating) return; + + _modelName = name; + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('modelName', name); + _modelSettings = await ModelSettingsProvider.loadStatic(modelName); + _updateModelOptions(); + notifyListeners(); + } + + /// Updates the global override settings for the current model. + /// + /// Returns `void`. void _updateModelOptions() { int? numGPU; @@ -719,18 +862,9 @@ class ChatProvider extends ChangeNotifier { _chat = ChatOllama(defaultOptions: modelOptions); } - Future setModel(String name) async { - if (isGenerating) return; - - _modelName = name; - final prefs = await SharedPreferences.getInstance(); - await prefs.setString('modelName', name); - _modelSettings = await ModelSettingsProvider.loadStatic(modelName); - _updateModelOptions(); - - notifyListeners(); - } - + /// Global override for setting the temperature. + /// + /// Returns `void`. void setTemperature(double value) async { if (isGenerating) return; @@ -745,6 +879,9 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } + /// Global override for setting the keep alive time. + /// + /// Returns `void`. void setKeepAliveTime(int value) async { if (isGenerating) return; @@ -759,6 +896,9 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } + /// Global override for enabling GPU usage. + /// + /// Returns `void`. void enableGPU(bool value) async { if (isGenerating) return; @@ -773,6 +913,9 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } + /// Enables or disables the display of performance statistics for the current chat. + /// + /// Returns `void`. void enableStatistics(bool value) async { _showStatistics = value; final prefs = await SharedPreferences.getInstance(); @@ -780,6 +923,9 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } + /// Global override for enabling web search. + /// + /// Returns `void`. void enableWebSearch(bool value) async { if (isGenerating) return; @@ -794,6 +940,9 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } + /// Global override for enabling docs search. + /// + /// Returns `void`. void enableDocsSearch(bool value) async { if (isGenerating) return; @@ -808,8 +957,14 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); } - // Helpers + /////////////////////////////////////////// + // Performance statistics // + /////////////////////////////////////////// + /// Computes the performance statistics of the last message. + /// + /// The function extracts the metadata and usage statistics from the result and adds them to the last message. + /// For metadata and usage statistics see [ChatResult.metadata] in langchain.dart. void _computePerformanceStatistics(ChatResult result) { lastMessage!.totalDuration += result.metadata['total_duration'] as int? ?? 0; @@ -825,7 +980,9 @@ class ChatProvider extends ChangeNotifier { lastMessage!.totalTokens += result.usage.totalTokens ?? 0; } - // Getters + /////////////////////////////////////////// + // Getters and setters // + /// /////////////////////////////////////// String get modelName => _modelName; diff --git a/app/lib/backend/providers/locale.dart b/app/lib/backend/providers/locale.dart index 5f56af7..0bc6ee5 100644 --- a/app/lib/backend/providers/locale.dart +++ b/app/lib/backend/providers/locale.dart @@ -5,6 +5,11 @@ import 'package:language_code/language_code.dart'; import 'package:open_local_ui/core/logger.dart'; import 'package:shared_preferences/shared_preferences.dart'; +/// A provider class for managing the application's locale. +/// +/// This class provides access to the user's selected locale, language code, currency name and symbol. +/// +/// This class extends the [ChangeNotifier] class, allowing it to notify listeners when the model settings change. class LocaleProvider extends ChangeNotifier { static const systemLangCode = 'system'; @@ -52,6 +57,7 @@ class LocaleProvider extends ChangeNotifier { notifyListeners(); } + /// Sets the user's selected language and saves it to shared preferences. Future setLanguage(String languageCode) async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('locale', languageCode); diff --git a/app/lib/backend/providers/model.dart b/app/lib/backend/providers/model.dart index e3f8e5c..f59a073 100644 --- a/app/lib/backend/providers/model.dart +++ b/app/lib/backend/providers/model.dart @@ -22,16 +22,27 @@ enum ModelProviderStatus { creating, } +/// A provider class for managing Ollama models. +/// +/// This class extends the [ChangeNotifier] class, allowing it to notify listeners when the models list changes. +/// +/// This class enables direct interaction with the Ollama API to pull, push, create and remove models. +/// +/// NOTE: You'll see some methods having a `Static` suffix (see [_updateListStatic]). This is because they are used outside the widget tree where providers are not accessible. class ModelProvider extends ChangeNotifier { static const _api = 'http://localhost:11434/api'; static final List _models = []; static late Process _process; ModelProviderStatus _status = ModelProviderStatus.idle; - static Future startOllama() async { + /// Start the Ollama server. + /// + /// This methods also redirects the stdout and stderr of the Ollama server to the logger. + /// + /// Returns a [Future] that resolves when the Ollama server is started. + static Future startOllamaStatic() async { try { - // Check if ollama is up and running - if (!await _isOlamaRunning()) { + if (!await _isOllamaRunningStatic()) { Process.start('ollama', ['serve']).then((Process process) { _process = process; @@ -59,7 +70,10 @@ class ModelProvider extends ChangeNotifier { } } - static Future _isOlamaRunning() async { + /// Check if the Ollama server is running. + /// + /// Returns a [Future] that resolves to a [bool] indicating whether the Ollama server is running. + static Future _isOllamaRunningStatic() async { try { final response = await HTTPHelpers.get('$_api/ps'); return response.statusCode == HttpStatus.ok; @@ -68,10 +82,18 @@ class ModelProvider extends ChangeNotifier { } } - static Future stopOllama() async { - _process.kill(); + /// Stop the Ollama server. + /// + /// This method sends a SIGKILL signal to the Ollama server process. + /// + /// Returns a void once the Ollama server is stopped. + static void stopOllamaStatic() async { + _process.kill(ProcessSignal.sigkill); } + /// Update the list of models. + /// + /// Returns a [Future] that resolves to null when the models list is updated. static Future _updateListStatic() async { await HTTPHelpers.get('$_api/tags').then((response) { if (response.statusCode != 200) { @@ -94,12 +116,20 @@ class ModelProvider extends ChangeNotifier { }); } + /// Update the list of models. Wraps the static method [_updateListStatic] and notifies listeners. + /// + /// Returns a [Future] that resolves to null when the models list is updated. Future updateList() async { await _updateListStatic(); notifyListeners(); } + /// Pull a model from the Ollama registry. + /// + /// The [name] parameter is the name of the model to pull. + /// + /// Returns a [Stream] of [OllamaPullResponse] objects. Stream pull(String name) async* { final completer = Completer(); @@ -206,6 +236,11 @@ class ModelProvider extends ChangeNotifier { completer.complete(); } + /// Push a model to the Ollama registry. + /// + /// The [name] parameter is the name of the model to push. + /// + /// Returns a [Stream] of [OllamaPushResponse] objects. Stream push(String name) async* { final completer = Completer(); @@ -312,6 +347,12 @@ class ModelProvider extends ChangeNotifier { completer.complete(); } + /// Create an Ollama model on local machine using a modelfile. + /// + /// The [name] parameter is the name of the model to create. + /// The [modelfile] parameter is the string containing the model configuration (see https://github.com/ollama/ollama/blob/main/docs/modelfile.md). + /// + /// Returns a [Stream] of [OllamaCreateResponse] objects. Stream create(String name, String modelfile) async* { final completer = Completer(); @@ -417,15 +458,24 @@ class ModelProvider extends ChangeNotifier { completer.complete(); } + /// Remove an Ollama model from local machine. + /// + /// The [name] parameter is the name of the model to remove. + /// + /// Returns a [Future] that resolves when the model is removed. Future remove(String name) async { try { - final response = - await HTTPHelpers.delete('$_api/delete', body: {'name': name}); + final response = await HTTPHelpers.delete( + '$_api/delete', + body: {'name': name}, + ); + if (response.statusCode != 200) { logger.e( 'Failed to remove model $name, status code: ${response.statusCode}'); return; } + await ModelSettingsProvider.removeStatic(name); logger.i('Model $name removed'); @@ -436,10 +486,13 @@ class ModelProvider extends ChangeNotifier { await updateList(); } - List get models => _models; - + /// Get models list. + /// + /// Returns a [List] of [Model] objects. static List getModelsStatic() => _models; + List get models => _models; + int get modelsCount => _models.length; bool get isPulling => _status == ModelProviderStatus.pulling; diff --git a/app/lib/backend/providers/model_settings.dart b/app/lib/backend/providers/model_settings.dart index b97f2f2..9b5cd49 100644 --- a/app/lib/backend/providers/model_settings.dart +++ b/app/lib/backend/providers/model_settings.dart @@ -7,6 +7,11 @@ import 'package:flutter/foundation.dart'; import 'package:open_local_ui/backend/models/model.dart'; import 'package:path_provider/path_provider.dart'; +/// A provider class for managing model settings. +/// +/// This class extends the [ChangeNotifier] class, allowing it to notify listeners when the model settings change. +/// +/// /// NOTE: You'll see some methods having a `Static` suffix (see [loadStatic]). This is because they are used outside the widget tree where providers are not accessible. class ModelSettingsProvider extends ChangeNotifier { final String modelName; late ModelSettings _settings; @@ -14,12 +19,9 @@ class ModelSettingsProvider extends ChangeNotifier { ModelSettingsProvider(this.modelName); - Future load() async { - _settings = await loadStatic(modelName); - notifyListeners(); - return _settings; - } - + /// Loads the settings for the given model. + /// + /// Returns a [Future] that evaluates to the [ModelSettings] object when the settings are loaded. static Future loadStatic(String modelName) async { final settingsFile = await _getSettingsFile(modelName); @@ -32,6 +34,20 @@ class ModelSettingsProvider extends ChangeNotifier { return ModelSettings.fromJson({}); } + /// Loads the settings for the given model. Wraps [loadStatic] and notifies listeners. + /// + /// Returns a [Future] that evaluates to the [ModelSettings] object when the settings are loaded. + Future load() async { + _settings = await loadStatic(modelName); + notifyListeners(); + return _settings; + } + + /// Returns the value of a setting. + /// + /// The [settingName] parameter is the name of the setting to get. + /// + /// Returns the value of the setting. dynamic get(String settingName) { switch (settingName) { case 'systemPrompt': @@ -107,7 +123,14 @@ class ModelSettingsProvider extends ChangeNotifier { } } - Future set(String settingName, dynamic newValue) async { + /// Sets the value of a setting. + /// + /// The [settingName] parameter is the name of the setting to set, and the [newValue] parameter is the new value of the setting. + /// + /// This sets the dirty flag to `true`. The settings are not saved until the [save] method is called. + /// + /// Return a [Future] that evaluates to `null` when the setting is set. + Future set(String settingName, dynamic newValue) async { switch (settingName) { case 'systemPrompt': _settings.systemPrompt = newValue; @@ -187,7 +210,29 @@ class ModelSettingsProvider extends ChangeNotifier { notifyListeners(); } - Future save() async { + /// Returns the settings file for the given model. + /// + /// The [modelName] parameter is the name of the model. + /// + /// You can find the model in the application support directory under the `models` directory with name `modelName.json`. + /// + /// Returns a [Future] that evaluates to the settings file. + static Future _getSettingsFile(String modelName) async { + final dir = await getApplicationSupportDirectory(); + final cleanName = modelName.toLowerCase().replaceAll(RegExp(r'\W'), '_'); + final settingsFile = File('${dir.path}/models/$cleanName.json'); + + return settingsFile; + } + + /// Saves the settings to the settings file. + /// + /// This resets the dirty flag to `false`. + /// + /// You can find the settings file (see [_getSettingsFile]). + /// + /// Return a [Future] that evaluates to `null` when the settings have been saved. + Future save() async { _isDirty = false; final settingsFile = await _getSettingsFile(modelName); @@ -203,7 +248,10 @@ class ModelSettingsProvider extends ChangeNotifier { notifyListeners(); } - Future reset() async { + /// Returns to the default settings. + /// + /// Retturn a [Future] that evaluates to `null` when the settings have been reset. + Future reset() async { _isDirty = false; final settingsFile = await _getSettingsFile(modelName); @@ -214,20 +262,18 @@ class ModelSettingsProvider extends ChangeNotifier { notifyListeners(); } - static Future _getSettingsFile(String modelName) async { - final dir = await getApplicationSupportDirectory(); - final cleanName = modelName.toLowerCase().replaceAll(RegExp(r'\W'), '_'); - final settingsFile = File('${dir.path}/models/$cleanName.json'); - - return settingsFile; - } - - bool get isDirty => _isDirty; - - static Future removeStatic(String name) async { + /// Removes the settings file for the given model. + /// + /// You can find the settings file (see [_getSettingsFile]). + /// + /// Return a [Future] that evaluates to `null` when the settings file is removed. + static Future removeStatic(String name) async { final settingsFile = await _getSettingsFile(name); if (await settingsFile.exists()) { await settingsFile.delete(); } } + + /// Returns whether the settings have been modified since the last save and have not been saved yet. + bool get isDirty => _isDirty; } diff --git a/app/lib/backend/services/tts.dart b/app/lib/backend/services/tts.dart index e47c12d..4e9f0d7 100644 --- a/app/lib/backend/services/tts.dart +++ b/app/lib/backend/services/tts.dart @@ -6,6 +6,11 @@ import 'package:open_local_ui/backend/services/protobufs/server.pbgrpc.dart'; import 'package:open_local_ui/core/logger.dart'; import 'package:path/path.dart' as p; +/// This class provides Text-to-Speech (TTS) functionality. +/// +/// This singleton class provides a communication channel to the TTS server written in Python through gRPC the protocol. +/// +/// NOTE: This class is instantiated in the main Isolate when execution begins. class TTSService { static late ClientChannel _channel; static late TTSClient _stub; diff --git a/app/lib/components/rive_animation.dart b/app/lib/components/rive_animation.dart index fc23bdc..45fe024 100644 --- a/app/lib/components/rive_animation.dart +++ b/app/lib/components/rive_animation.dart @@ -14,7 +14,8 @@ class RiveAnimationComponent extends StatefulWidget { final String darkArtboardName; final BoxFit fit; - const RiveAnimationComponent({super.key, + const RiveAnimationComponent({ + super.key, required this.assetPath, required this.animationName, required this.lightArtboardName, @@ -41,7 +42,7 @@ class _RiveAnimationComponentState extends State { Future _loadRiveAnimation(String filename) async { if (AssetManager.isAssetLoaded(filename)) { - final buffer = AssetManager.getAssetAsBytes(filename); + final buffer = AssetManager.getAsset(filename, type: AssetType.binary); final bytes = ByteData.view(buffer.buffer); await RiveFile.initialize(); return RiveFile.import(bytes); diff --git a/app/lib/core/asset.dart b/app/lib/core/asset.dart index 09b0d63..a5b0c47 100644 --- a/app/lib/core/asset.dart +++ b/app/lib/core/asset.dart @@ -1,62 +1,175 @@ import 'dart:convert'; import 'package:flutter/services.dart'; +import 'package:open_local_ui/core/http.dart'; import 'package:open_local_ui/core/logger.dart'; import 'package:shared_preferences/shared_preferences.dart'; -enum AssetSource { local, remote } +enum AssetSource { + local, + remote, +} -enum AssetType { text, image, audio, video, binary } +enum AssetType { + raw, + json, + binary, +} +/// Manages the assets used in the application. +/// +/// The [AssetManager] class provides methods for caching assets into an asset pool, +/// this way assets can be loaded from memory instead of the file system or network, +/// thefore gratly improving performance. +/// +/// NOTE: The pool is most effective with small sized and frequently accessed assets. class AssetManager { static final Map _assetRegistry = {}; - static Future loadLocalAsset(String assetPath) async { - final assetContent = await rootBundle.loadString(assetPath); - _assetRegistry[assetPath] = assetContent; - logger.d('Loaded asset: $assetPath'); - return assetContent; - } - + /// Wapper around [SharedPreferences] to save a key-value pair to the device's preferences. + /// + /// The [key] parameter should be a string representing the key of the value to be saved. + /// The [value] parameter should be a string representing the value to be saved. + /// + /// The method returns a [Future] that resolves to void. static Future saveToPreferences(String key, String value) async { final prefs = await SharedPreferences.getInstance(); logger.d('Saved to preferences: $key'); await prefs.setString(key, value); } - static Future getFromPreferences(String key) async { + /// Wapper around [SharedPreferences] to retrieve a value from the device's preferences. + /// + /// The [key] parameter should be a string representing the key of the value to be retrieved. + /// + /// The method returns a [Future] that resolves to the specified value type. + static Future getFromPreferences(String key) async { final prefs = await SharedPreferences.getInstance(); logger.d('Retrieved from preferences: $key'); - return prefs.getString(key); + switch (T) { + case String: + return prefs.getString(key) as T?; + case int: + return prefs.getInt(key) as T?; + case double: + return prefs.getDouble(key) as T?; + case bool: + return prefs.getBool(key) as T?; + default: + throw Exception('Invalid preference type'); + } } - static Map getAssetAsJson(String key) { - final assetContent = getRawAsset(key); + /// Retrieves an asset from the asset pool in JSON format. + /// + /// The [key] parameter should be a string representing the path of the asset to be retrieved. + /// + /// The method returns a [Map] that represents the asset in JSON format. + static Map _getAssetAsJson(String key) { + final assetContent = _getRawAsset(key); return jsonDecode(assetContent!); } - static Uint8List getAssetAsBytes(String key) { - final assetContent = getRawAsset(key); + /// Retrieves an asset from the asset pool in the binary format. + /// + /// The [key] parameter should be a string representing the path of the asset to be retrieved. + /// + /// The method returns a [Uint8List] that represents the asset in binary format. + static Uint8List _getAssetAsBytes(String key) { + final assetContent = _getRawAsset(key); return Uint8List.fromList(assetContent!.codeUnits); } - static String? getRawAsset(String key) { + /// Retrieves an asset from the asset pool in plain text format. + /// + /// The [key] parameter should be a string representing the path of the asset to be retrieved. + /// + /// The method returns a [String] that represents the asset in plain text format. + static String? _getRawAsset(String key) { logger.d('Retrieved asset: $key'); return _assetRegistry[key]; } + static dynamic getAsset( + String key, { + required AssetType type, + }) { + switch (type) { + case AssetType.raw: + return _getRawAsset(key); + case AssetType.json: + return _getAssetAsJson(key); + case AssetType.binary: + return _getAssetAsBytes(key); + default: + throw Exception('Invalid asset type'); + } + } + + /// Checks if an asset is loaded in the asset pool. + /// + /// The [key] parameter should be a string representing the path of the asset to be checked. + /// + /// The method returns a boolean value. + static bool isAssetLoaded(String key) { + return _assetRegistry.containsKey(key); + } + + /// Loads an asset from local storage or network and caches it into the asset pool. + /// + /// The [key] parameter should be a string representing the path of the asset to be loaded. + /// The [source] parameter should be an [AssetSource] enum representing the source of the asset. + /// The [type] parameter should be an [AssetType] enum representing the type of the asset. + /// The [forceReload] parameter should be a boolean value indicating if the asset should be reloaded if it already exists in the pool. + /// + /// The method returns a [Future] that resolves to the asset content in plain text format. + static Future loadAsset( + String key, { + required AssetSource source, + AssetType type = AssetType.raw, + bool forceReload = false, + }) async { + if (!isAssetLoaded(key) || (isAssetLoaded(key) && forceReload)) { + late String assetContent; + + switch (source) { + case AssetSource.local: + assetContent = await rootBundle.loadString(key); + break; + case AssetSource.remote: + assetContent = await HTTPHelpers.get(key).then( + (response) => response.body, + ); + break; + default: + throw Exception('Invalid asset source'); + } + + _assetRegistry[key] = assetContent; + logger.d('Loaded asset: $key'); + + return assetContent; + } else { + return getAsset(key, type: type)!; + } + } + + /// Unloads an asset from the asset pool. + /// + /// The [key] parameter should be a string representing the path of the asset to be unloaded. + /// + /// The method returns void. static void unloadAsset(String key) { logger.d('Unloaded asset: $key'); _assetRegistry.remove(key); } + /// Clears all assets from the asset pool. + /// + /// The method returns void. static void clearAssets() { logger.d('Cleared all assets'); _assetRegistry.clear(); } - - static bool isAssetLoaded(String key) { - return _assetRegistry.containsKey(key); - } } diff --git a/app/lib/core/color.dart b/app/lib/core/color.dart index 0119f00..62f8472 100644 --- a/app/lib/core/color.dart +++ b/app/lib/core/color.dart @@ -1,11 +1,23 @@ import 'package:flutter/material.dart'; +/// A helper class for working with colors. class ColorHelpers { + /// Converts a hexadecimal color code to a Flutter [Color] object. + /// + /// The [hex] parameter should be a string representing a hexadecimal color code, + /// with or without the '#' symbol. + /// + /// Returns a [Color] object representing the converted color. static Color colorFromHex(String hex) { final hexCode = hex.replaceAll('#', ''); return Color(int.parse('FF$hexCode', radix: 16)); } + /// Converts a Flutter [Color] object to a hexadecimal color code. + /// + /// The [color] parameter should be a [Color] object representing a color. + /// + /// Returns a string representing the hexadecimal color code, including the '#' symbol. static String colorToHex(Color color) { return '#${color.value.toRadixString(16).substring(2).toUpperCase()}'; } diff --git a/app/lib/core/format.dart b/app/lib/core/format.dart index acba91b..f6d88d2 100644 --- a/app/lib/core/format.dart +++ b/app/lib/core/format.dart @@ -1,6 +1,10 @@ import 'package:intl/intl.dart'; +/// A helper class for working with date, time, number, units of measure... formats. class FortmatHelpers { + /// Formats the given [dateTime] into a standard date string. + /// + /// Returns a string representation of the [dateTime] in the format: "YYYY-MM-DD". static String standardDate(DateTime dateTime) { return DateFormat("dd/MM/yyyy HH:mm:ss").format(dateTime); } diff --git a/app/lib/core/github.dart b/app/lib/core/github.dart index 837c3af..8a1e536 100644 --- a/app/lib/core/github.dart +++ b/app/lib/core/github.dart @@ -8,6 +8,7 @@ import 'package:package_info_plus/package_info_plus.dart'; part 'github.g.dart'; +/// Represents a release asset on GitHub. @JsonSerializable() class GitHubReleaseAsset { final String name; @@ -26,6 +27,7 @@ class GitHubReleaseAsset { Map toJson() => _$GitHubReleaseAssetToJson(this); } +/// Represents a release on GitHub.s @JsonSerializable() class GitHubRelease { final String name; @@ -45,6 +47,7 @@ class GitHubRelease { Map toJson() => _$GitHubReleaseToJson(this); } +/// Represents a GitHub contributor account. @JsonSerializable() class GitHubContributor { final String login; @@ -67,12 +70,18 @@ class GitHubContributor { Map toJson() => _$GitHubContributorToJson(this); } +/// A class to interact with the GitHub API. +/// +/// Authenticated requests are made using Personal Access Tokens (PATs) found in the `.env` file and baked into the app during build time. class GitHubAPI { static const owner = 'WilliamKarolDiCioccio'; static const repo = 'open_local_ui'; + /// Gets the latest release of the repository. + /// + /// This is used to check for app updates. static Future getLatestRelease() async { - // NOTE: We'll switch to the releases/latest endpoint once we have a release as pre-release and draft releases are not considered as latest by the API + // We'll switch to the releases/latest endpoint once we have a release as pre-release and draft releases are not considered as latest by the API final url = Uri.parse( 'https://api.github.com/repos/$owner/$repo/releases', ); @@ -96,8 +105,9 @@ class GitHubAPI { final List decodedJson = jsonDecode(response.body); - final GitHubRelease latestRelease = - GitHubRelease.fromJson(decodedJson.first); + final GitHubRelease latestRelease = GitHubRelease.fromJson( + decodedJson.first, + ); logger.d( 'Latest release fetched successfully. Latest release: $latestRelease', @@ -106,6 +116,44 @@ class GitHubAPI { return latestRelease; } + /// Get a list of all releases of the repository. + /// + /// This is used to check for app updates if in the latest release the platform specific update is not available. + static Future> listReleases() async { + final url = Uri.parse( + 'https://api.github.com/repos/$owner/$repo/releases', + ); + + final headers = { + 'Authorization': 'token ${Env.gitHubReleasesPat}', + 'Accept': 'application/vnd.github+json', + 'Content-Type': 'application/json', + 'X-GitHub-Api-Version': '2022-11-28' + }; + + final response = await http.get(url, headers: headers); + + if (response.statusCode != 200) { + logger.d('Failed to list releases. Status code: ${response.statusCode}'); + + return []; + } + + final List decodedJson = jsonDecode(response.body); + + final List releases = []; + + for (final release in decodedJson) { + releases.add(GitHubRelease.fromJson(release)); + } + + logger.d('Releases listed successfully. Releases: $releases'); + + return releases; + } + + /// Lists the contributors of the repository. + /// This is used in our about page to ensure we give credits to everyone who works or worked on the app. static Future> listRepositoryContributors() async { final url = Uri.parse( 'https://api.github.com/repos/$owner/$repo/contributors', @@ -143,6 +191,17 @@ class GitHubAPI { return contributors; } + /// Creates a new issue on the repository. + /// This is used to report issues from the app feedback form. + /// + /// The [text] parameter is the issue body text. + /// The [screenshotUrl] parameter is the URL of the screenshot to attach to the issue. + /// The [logsUrl] parameter is the URL of the logs file to attach to the issue. + /// The [deviceInfo] parameter is the device information to attach to the issue. + /// + /// NOTE: There is currently no way to attach files to issues via the GitHub API. + /// As a workaround, we're attaching the screenshot and logs as links in the issue body. + /// In the case of our feedback form, we're uploading the screenshot and logs to a SupaBase storage bucket and using the URLs in the issue body. static Future createGitHubIssue( String text, String screenshotUrl, diff --git a/app/lib/core/http.dart b/app/lib/core/http.dart index 9963cba..f8292e2 100644 --- a/app/lib/core/http.dart +++ b/app/lib/core/http.dart @@ -5,6 +5,9 @@ import 'package:http/http.dart' as http; part 'http.g.dart'; +/// Represents an HTTP single response with embedded metadata. +/// +/// This classe is marked as `@JsonSerializable`. @JsonSerializable() class HTTPResponse { final String status; @@ -21,6 +24,9 @@ class HTTPResponse { Map toJson() => _$HTTPResponseToJson(this); } +/// Represents an HTTP stream response with emdedded metadata. +/// +/// This class is marked as `@JsonSerializable`. @JsonSerializable() class HTTPStreamResponse extends HTTPResponse { final int total; @@ -42,21 +48,61 @@ class HTTPStreamResponse extends HTTPResponse { Map toJson() => _$HTTPStreamResponseToJson(this); } +/// A helper class for handling HTTP requests. +/// +/// The [HTTPHelpers] class provides basic wrappers around the `http` package for making HTTP requests. class HTTPHelpers { + /// Sends a GET request to the given [url]. + /// + /// See https://restfulapi.net/http-methods/#get. static Future get(String url) async { return http.get(Uri.parse(url)); } - static Future post(String url, - {Map? body}) async { + /// Sends a POST request to the given [url]. + /// + /// See https://restfulapi.net/http-methods/#post. + static Future post( + String url, { + Map? body, + }) async { return http.post(Uri.parse(url), body: jsonEncode(body)); } - static Future delete(String url, - {Map? body}) async { + /// Sends a PUT request to the given [url]. + /// + /// See https://restfulapi.net/http-methods/#put. + static Future put( + String url, { + Map? body, + }) async { + return http.put(Uri.parse(url), body: jsonEncode(body)); + } + + /// Sends a PUT request to the given [url]. + /// + /// See https://restfulapi.net/http-methods/#delete. + static Future delete( + String url, { + Map? body, + }) async { return http.delete(Uri.parse(url), body: jsonEncode(body)); } + /// Sends a PATCH request to the given [url]. + /// + /// See https://restfulapi.net/http-methods/#patch. + static Future patch( + String url, { + Map? body, + }) async { + return http.patch(Uri.parse(url), body: jsonEncode(body)); + } + + /// Calculates the remaining time for the given [response] stream to complete. + /// + /// The [response] parameter should be an instance of [HTTPStreamResponse]. + /// Returns a [Duration] representing the remaining time. static Duration calculateRemainingTime(HTTPStreamResponse response) { final remainingBytes = response.total - response.completed; diff --git a/app/lib/core/image.dart b/app/lib/core/image.dart index e78c207..455eecc 100644 --- a/app/lib/core/image.dart +++ b/app/lib/core/image.dart @@ -1,6 +1,11 @@ import 'dart:typed_data'; import 'package:image/image.dart' as img; +/// Manages the caching of images. +/// +/// This class provides methods for caching images into an image pool. +/// The scope of this class is much narrower than the [AssetManager] class, and its not globally available but instead should be instantiated. +/// We're evauluating the option to merge the two classes into one. class ImageCacheManager { static final ImageCacheManager _instance = ImageCacheManager._internal(); factory ImageCacheManager() => _instance; @@ -22,7 +27,13 @@ class ImageCacheManager { } } +/// A helper class for working with images. class ImageHelpers { + /// Compares two images represented as [Uint8List] objects. + /// + /// The [imageBytes1] and [imageBytes2] parameters should be [Uint8List] objects representing the images to be compared. + /// + /// Returns a [Future] that resolves to a [bool] indicating whether the images are pixel-perfect identical. static Future compare( Uint8List? imageBytes1, Uint8List? imageBytes2) async { img.Image? image1 = img.decodeImage(imageBytes1 ?? Uint8List(0)); diff --git a/app/lib/core/logger.dart b/app/lib/core/logger.dart index 96e0172..358fb6d 100644 --- a/app/lib/core/logger.dart +++ b/app/lib/core/logger.dart @@ -9,10 +9,10 @@ import 'package:path_provider/path_provider.dart'; late Logger logger; late File _logFile; -class CombinedOutput extends LogOutput { +class _CombinedOutput extends LogOutput { final List _outputs; - CombinedOutput(this._outputs); + _CombinedOutput(this._outputs); @override void output(OutputEvent event) { @@ -22,14 +22,20 @@ class CombinedOutput extends LogOutput { } } +/// This function initializes the logger and its executed when the application starts. +/// +/// The logger is configured to output logs to the console and a log file, in debug mode, and only to the log file in release mode. +/// The log level is set to [Level.all] in debug mode and [Level.warning] in release mode. +/// +/// The log file is stored in the application's support directory (see the output of [getApplicationSupportDirectory]). Future initLogger() async { late LogOutput logOutput; - _logFile = await createLogFile(); + _logFile = await _createLogFile(); Level logLevel = kDebugMode ? Level.all : Level.warning; if (kDebugMode) { - logOutput = CombinedOutput([ConsoleOutput(), FileOutput(file: _logFile)]); + logOutput = _CombinedOutput([ConsoleOutput(), FileOutput(file: _logFile)]); } else { logOutput = FileOutput(file: _logFile); } @@ -46,7 +52,7 @@ Future initLogger() async { ); } -Future createLogFile() async { +Future _createLogFile() async { final timeStamp = FortmatHelpers.standardDate(DateTime.now()) .replaceAll(' ', '_') .replaceAll('/', '-') diff --git a/app/lib/core/process.dart b/app/lib/core/process.dart index 023a2a6..0f3b31d 100644 --- a/app/lib/core/process.dart +++ b/app/lib/core/process.dart @@ -10,7 +10,12 @@ class _IsolateData { _IsolateData(this.sendPort, this.command, this.arguments); } +/// A helper class for working with processes and shell commands. class ProcessHelpers { + /// Runs a shell command and returns the output as a string. + /// + /// Returns a [Future] that completes with the output of the shell command. + /// The command is executed in a separate isolate to avoid blocking the main thread. static Future runShellCommand( String command, { List? arguments, @@ -23,6 +28,10 @@ class ProcessHelpers { return await receivePort.first; } + /// Runs a process in a detached mode. + /// + /// Returns a [Future] that completes with a [ProcessResult] when the process finishes. + /// The command is executed in a separate isolate to avoid blocking the main thread. static Future runDetached( String executable, { List? arguments, diff --git a/app/lib/core/update.dart b/app/lib/core/update.dart index e68b969..3e27aed 100644 --- a/app/lib/core/update.dart +++ b/app/lib/core/update.dart @@ -11,10 +11,25 @@ import 'package:open_local_ui/frontend/helpers/snackbar.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; +/// A helper class for handling updates. +/// +/// The [UpdateHelper] class provides methods for checking and installing updates for the application and the Ollama tool. class UpdateHelper { static late GitHubRelease _latestRelease; + /// Checks if a new version of the Ollama tool is available. + /// + /// Returns a [Future] that resolves to a [bool] indicating whether a new version is available. + /// + /// The method dispatches the check to the platform-specific method. static Future isOllamaUpdateAvailable() async { + if (_windowsIsOllamaUpdateAvailable()) return true; + + return false; + } + + /// The method uses the `winget` command to check for updates. + static _windowsIsOllamaUpdateAvailable() async { final wingetUpgradesList = await ProcessHelpers.runShellCommand( 'winget', arguments: ['upgrade'], @@ -25,6 +40,11 @@ class UpdateHelper { return false; } + /// Downloads and installs the latest version of the Ollama tool. + /// + /// Returns a [Future] that resolves to `null`. + /// + /// The method dispatches the installation to the platform-specific method. static Future downloadAndInstallOllamaLatestVersion() async { if (Platform.isWindows) { await _windowsDownloadAndInstallOllama(); @@ -34,6 +54,7 @@ class UpdateHelper { } } + /// The method uses the `winget` command to download and install the latest version of the Ollama tool. static Future _windowsDownloadAndInstallOllama() async { final wingetInstallResult = await ProcessHelpers.runShellCommand( 'winget', @@ -53,6 +74,7 @@ class UpdateHelper { } } + /// Helper method to check if a superior is available according to our versioning scheme. static bool _isVersionSuperior(String version) { final currentVersion = Env.version.split('.').map(int.parse).toList(); final newVersion = version.split('.').map(int.parse).toList(); @@ -66,6 +88,12 @@ class UpdateHelper { return false; } + /// Checks if a new version of the application is available. + /// + /// Returns a [Future] that resolves to a [bool] indicating whether a new version is available. + /// + /// The method uses the [GitHubAPI] class to fetch the latest release and compares it to the current version. + /// It then dispatches the check to the platform-specific method. static Future isAppUpdateAvailable() async { if (!Platform.isWindows) { logger.i( @@ -75,41 +103,50 @@ class UpdateHelper { return false; } - _latestRelease = await GitHubAPI.getLatestRelease(); + final releases = await GitHubAPI.listReleases(); - final prefs = await SharedPreferences.getInstance(); - - final latestAvailableVersion = _latestRelease.tag_name; - - if (latestAvailableVersion.isEmpty) { - logger.i('Latest release not found on GitHub'); - return false; - } - if (prefs.getString('skipUpdate') == latestAvailableVersion) { - logger.i('Skipping update: $latestAvailableVersion'); - return false; - } else if (!_isVersionSuperior(latestAvailableVersion)) { - logger.i('No new version available'); + if (releases.isEmpty) { + logger.e('Failed to list releases'); return false; } - logger.i('New version available: $latestAvailableVersion'); + for (final release in releases) { + final prefs = await SharedPreferences.getInstance(); - for (final asset in _latestRelease.assets) { - if (Platform.isWindows && asset.name.contains('windows_x64')) { - return true; + if (prefs.getString('skipUpdate') == release.tag_name) { + logger.i('Skipping update: $release.tag_name'); + continue; + } else if (!_isVersionSuperior(release.tag_name)) { + logger.i('No new version available'); + break; + } + + logger.i('New version available: $release.tag_name'); + + for (final asset in release.assets) { + if (Platform.isWindows && _windowsIsAppUpdateAvailable(asset)) { + return true; + } } } return false; } + /// Helper method to check if the asset is an update for the Windows platform. + static bool _windowsIsAppUpdateAvailable(GitHubReleaseAsset asset) => + asset.name.contains('windows_x64'); + + /// Skips the update for the current version. The method stores the version in the shared preferences. static Future skipUpdate() async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('skipUpdate', _latestRelease.tag_name); } + /// Downloads and installs the latest version of the application. + /// + /// The method dispatches the installation to the platform-specific method. static Future downloadAndInstallAppLatestVersion() async { if (Platform.isWindows) { await _windowsDownloadAndInstallApp(); @@ -119,6 +156,7 @@ class UpdateHelper { } } + /// Downloads and installs the latest Windows version of the application. static Future _windowsDownloadAndInstallApp() async { GitHubReleaseAsset? installer; diff --git a/app/lib/frontend/pages/dashboard/about.dart b/app/lib/frontend/pages/dashboard/about.dart index 31f031a..a760964 100644 --- a/app/lib/frontend/pages/dashboard/about.dart +++ b/app/lib/frontend/pages/dashboard/about.dart @@ -67,7 +67,10 @@ class AboutPage extends StatelessWidget { tooltip: AppLocalizations.of(context) .aboutPageSocialButtonWatchTrailerTooltip, onPressed: () { - // TODO: Implement YouTube trailer link + launchUrl( + // WARNING: do not open if you don't want to be rickrolled! + Uri.parse('https://www.youtube.com/watch?v=dQw4w9WgXcQ'), + ); }, icon: const Icon(UniconsLine.youtube), iconSize: 44, diff --git a/app/lib/frontend/pages/dashboard/models.dart b/app/lib/frontend/pages/dashboard/models.dart index ec1a15a..6273cad 100644 --- a/app/lib/frontend/pages/dashboard/models.dart +++ b/app/lib/frontend/pages/dashboard/models.dart @@ -352,7 +352,7 @@ class _ModelListTileState extends State { final cleanModelName = modelName.toLowerCase().split(':')[0]; - final metadata = AssetManager.getAssetAsJson(metadataPath); + final metadata = AssetManager.getAsset(metadataPath, type: AssetType.json); if (!metadata['models'].containsKey(cleanModelName)) { return const SizedBox.shrink(); diff --git a/app/lib/frontend/widgets/chat_input_field.dart b/app/lib/frontend/widgets/chat_input_field.dart index 8cb7ec1..5024f3f 100644 --- a/app/lib/frontend/widgets/chat_input_field.dart +++ b/app/lib/frontend/widgets/chat_input_field.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:gap/gap.dart'; diff --git a/app/lib/frontend/widgets/markdown_code_wrapper.dart b/app/lib/frontend/widgets/markdown_code_wrapper.dart index ca570d3..4302f91 100644 --- a/app/lib/frontend/widgets/markdown_code_wrapper.dart +++ b/app/lib/frontend/widgets/markdown_code_wrapper.dart @@ -155,8 +155,9 @@ class _CodeWrapperState extends State { Tooltip( message: widget.language.toUpperCase(), child: SvgPicture.memory( - AssetManager.getAssetAsBytes( + AssetManager.getAsset( languageToAsset[widget.language]!, + type: AssetType.binary, ), width: 20, height: 20, diff --git a/app/lib/main.dart b/app/lib/main.dart index aa9ee15..ef749b8 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -29,60 +29,114 @@ import 'package:system_theme/system_theme.dart'; void _preloadAssets() async { Future.wait( [ - AssetManager.loadLocalAsset('assets/graphics/animations/gpu.riv'), - AssetManager.loadLocalAsset('assets/graphics/animations/human.riv'), - AssetManager.loadLocalAsset('assets/metadata/ollama_models.json'), - AssetManager.loadLocalAsset('assets/graphics/logos/apache.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/arduino.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/bash.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/c.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/clojure.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/cmake.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/cpp.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/crystal.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/cs.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/css.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/dart.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/delphi.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/dockerfile.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/elixir.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/erlang.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/flutter.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/fortran.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/glsl.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/go.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/gradle.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/haskell.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/java.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/javascript.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/json.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/julia.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/kotlin.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/langchain.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/less.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/llvm.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/lua.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/makefile.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/nginx.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/nsis.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/ocaml.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/ollama.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/perl.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/php.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/powershell.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/python.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/ruby.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/rust.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/scala.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/scss.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/supabase.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/swift.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/toml.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/typescript.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/vala.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/xml.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/html.svg'), - AssetManager.loadLocalAsset('assets/graphics/logos/yaml.svg'), + AssetManager.loadAsset('assets/graphics/animations/gpu.riv', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/animations/human.riv', + source: AssetSource.local), + AssetManager.loadAsset('assets/metadata/ollama_models.json', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/apache.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/arduino.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/bash.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/c.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/clojure.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/cmake.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/cpp.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/crystal.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/cs.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/css.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/dart.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/delphi.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/dockerfile.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/elixir.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/erlang.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/flutter.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/fortran.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/glsl.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/go.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/gradle.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/haskell.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/java.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/javascript.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/json.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/julia.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/kotlin.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/langchain.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/less.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/llvm.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/lua.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/makefile.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/nginx.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/nsis.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/ocaml.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/ollama.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/perl.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/php.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/powershell.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/python.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/ruby.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/rust.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/scala.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/scss.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/supabase.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/swift.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/toml.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/typescript.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/vala.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/xml.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/html.svg', + source: AssetSource.local), + AssetManager.loadAsset('assets/graphics/logos/yaml.svg', + source: AssetSource.local), ], ).then((_) { logger.i('Assets preloaded'); @@ -97,7 +151,7 @@ void main() async { await initLogger(); - await ModelProvider.startOllama(); + await ModelProvider.startOllamaStatic(); await TTSService.startServer(); await ChatSessionsDatabase.init(); @@ -204,7 +258,7 @@ class _MyAppState extends State { @override void dispose() { TTSService.stopServer(); - ModelProvider.stopOllama(); + ModelProvider.stopOllamaStatic(); ChatSessionsDatabase.deinit(); super.dispose(); From f6e88b55c9c8e3cc0833c81554f745f765da9e2f Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Wed, 21 Aug 2024 22:22:56 +0200 Subject: [PATCH 04/13] Move Rive engine initialization to main.dart --- app/lib/components/rive_animation.dart | 1 - app/lib/main.dart | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/app/lib/components/rive_animation.dart b/app/lib/components/rive_animation.dart index 45fe024..e3c1fc8 100644 --- a/app/lib/components/rive_animation.dart +++ b/app/lib/components/rive_animation.dart @@ -44,7 +44,6 @@ class _RiveAnimationComponentState extends State { if (AssetManager.isAssetLoaded(filename)) { final buffer = AssetManager.getAsset(filename, type: AssetType.binary); final bytes = ByteData.view(buffer.buffer); - await RiveFile.initialize(); return RiveFile.import(bytes); } else { final bytes = await rootBundle.load(filename); diff --git a/app/lib/main.dart b/app/lib/main.dart index ef749b8..4dc1497 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -22,6 +22,7 @@ import 'package:open_local_ui/env.dart'; import 'package:open_local_ui/frontend/screens/dashboard.dart'; import 'package:open_local_ui/frontend/screens/onboarding.dart'; import 'package:provider/provider.dart'; +import 'package:rive/rive.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:system_theme/system_theme.dart'; @@ -168,6 +169,8 @@ void main() async { _preloadAssets(); + RiveFile.initialize(); + // Theme if (defaultTargetPlatform.supportsAccentColor) { From dc1286b453c63007f84b1a680f00587031eede74 Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:15:00 +0200 Subject: [PATCH 05/13] Moved components directory --- app/lib/{ => frontend}/components/rive_animation.dart | 0 app/lib/{ => frontend}/components/typewriter_text.dart | 0 app/lib/frontend/screens/onboarding.dart | 4 ++-- 3 files changed, 2 insertions(+), 2 deletions(-) rename app/lib/{ => frontend}/components/rive_animation.dart (100%) rename app/lib/{ => frontend}/components/typewriter_text.dart (100%) diff --git a/app/lib/components/rive_animation.dart b/app/lib/frontend/components/rive_animation.dart similarity index 100% rename from app/lib/components/rive_animation.dart rename to app/lib/frontend/components/rive_animation.dart diff --git a/app/lib/components/typewriter_text.dart b/app/lib/frontend/components/typewriter_text.dart similarity index 100% rename from app/lib/components/typewriter_text.dart rename to app/lib/frontend/components/typewriter_text.dart diff --git a/app/lib/frontend/screens/onboarding.dart b/app/lib/frontend/screens/onboarding.dart index fd0646f..ced9a0b 100644 --- a/app/lib/frontend/screens/onboarding.dart +++ b/app/lib/frontend/screens/onboarding.dart @@ -11,8 +11,8 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:gap/gap.dart'; import 'package:gpu_info/gpu_info.dart'; import 'package:introduction_screen/introduction_screen.dart'; -import 'package:open_local_ui/components/rive_animation.dart'; -import 'package:open_local_ui/components/typewriter_text.dart'; +import 'package:open_local_ui/frontend/components/rive_animation.dart'; +import 'package:open_local_ui/frontend/components/typewriter_text.dart'; import 'package:open_local_ui/core/color.dart'; import 'package:open_local_ui/core/process.dart'; import 'package:open_local_ui/frontend/dialogs/color_picker.dart'; From 0e5e885cbaadace7bb43760460cf08677119e0c0 Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:17:13 +0200 Subject: [PATCH 06/13] Moved window_management_bar.dart to components --- .../{widgets => components}/window_management_bar.dart | 4 ++-- app/lib/frontend/screens/dashboard.dart | 4 ++-- app/lib/frontend/screens/onboarding.dart | 4 ++-- app/lib/frontend/screens/update_in_progress.dart | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename app/lib/frontend/{widgets => components}/window_management_bar.dart (90%) diff --git a/app/lib/frontend/widgets/window_management_bar.dart b/app/lib/frontend/components/window_management_bar.dart similarity index 90% rename from app/lib/frontend/widgets/window_management_bar.dart rename to app/lib/frontend/components/window_management_bar.dart index 35b0205..ffcb0ab 100644 --- a/app/lib/frontend/widgets/window_management_bar.dart +++ b/app/lib/frontend/components/window_management_bar.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:system_theme/system_theme.dart'; -class WindowManagementBar extends StatelessWidget { - const WindowManagementBar({super.key}); +class WindowManagementBarComponent extends StatelessWidget { + const WindowManagementBarComponent({super.key}); @override Widget build(BuildContext context) { diff --git a/app/lib/frontend/screens/dashboard.dart b/app/lib/frontend/screens/dashboard.dart index d7359b4..02ce5da 100644 --- a/app/lib/frontend/screens/dashboard.dart +++ b/app/lib/frontend/screens/dashboard.dart @@ -20,7 +20,7 @@ import 'package:open_local_ui/frontend/pages/dashboard/chat.dart'; import 'package:open_local_ui/frontend/pages/dashboard/models.dart'; import 'package:open_local_ui/frontend/pages/dashboard/sessions.dart'; import 'package:open_local_ui/frontend/pages/dashboard/settings.dart'; -import 'package:open_local_ui/frontend/widgets/window_management_bar.dart'; +import 'package:open_local_ui/frontend/components/window_management_bar.dart'; import 'package:path_provider/path_provider.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; import 'package:system_info2/system_info2.dart'; @@ -107,7 +107,7 @@ class _DashboardScreenState extends State { right: 0.0, width: MediaQuery.of(context).size.width, height: 32.0, - child: const WindowManagementBar(), + child: const WindowManagementBarComponent(), ), ], ); diff --git a/app/lib/frontend/screens/onboarding.dart b/app/lib/frontend/screens/onboarding.dart index ced9a0b..b522c75 100644 --- a/app/lib/frontend/screens/onboarding.dart +++ b/app/lib/frontend/screens/onboarding.dart @@ -19,7 +19,7 @@ import 'package:open_local_ui/frontend/dialogs/color_picker.dart'; import 'package:open_local_ui/frontend/helpers/snackbar.dart'; import 'package:open_local_ui/frontend/screens/dashboard.dart'; import 'package:open_local_ui/frontend/widgets/preference_selector.dart'; -import 'package:open_local_ui/frontend/widgets/window_management_bar.dart'; +import 'package:open_local_ui/frontend/components/window_management_bar.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:system_info2/system_info2.dart'; import 'package:system_theme/system_theme.dart'; @@ -265,7 +265,7 @@ class _OnboardingScreenState extends State { right: 0.0, width: MediaQuery.of(context).size.width, height: 32.0, - child: const WindowManagementBar(), + child: const WindowManagementBarComponent(), ), ], ); diff --git a/app/lib/frontend/screens/update_in_progress.dart b/app/lib/frontend/screens/update_in_progress.dart index 6cc4340..96d69be 100644 --- a/app/lib/frontend/screens/update_in_progress.dart +++ b/app/lib/frontend/screens/update_in_progress.dart @@ -5,7 +5,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; import 'package:gap/gap.dart'; import 'package:open_local_ui/core/update.dart'; -import 'package:open_local_ui/frontend/widgets/window_management_bar.dart'; +import 'package:open_local_ui/frontend/components/window_management_bar.dart'; class UpdateInProgressScreen extends StatefulWidget { const UpdateInProgressScreen({super.key}); @@ -57,7 +57,7 @@ class _UpdateInProgressScreenState extends State { right: 0.0, width: MediaQuery.of(context).size.width, height: 32.0, - child: const WindowManagementBar(), + child: const WindowManagementBarComponent(), ), ], ); From 6bf33e3a7762a335c6ae03081d1a2a56c161b387 Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:20:28 +0200 Subject: [PATCH 07/13] Fix WindowManagementBarComponent theming --- .../frontend/components/window_management_bar.dart | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/app/lib/frontend/components/window_management_bar.dart b/app/lib/frontend/components/window_management_bar.dart index ffcb0ab..28bc422 100644 --- a/app/lib/frontend/components/window_management_bar.dart +++ b/app/lib/frontend/components/window_management_bar.dart @@ -1,7 +1,7 @@ +import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:flutter/material.dart'; import 'package:bitsdojo_window/bitsdojo_window.dart'; -import 'package:system_theme/system_theme.dart'; class WindowManagementBarComponent extends StatelessWidget { const WindowManagementBarComponent({super.key}); @@ -18,19 +18,22 @@ class WindowManagementBarComponent extends StatelessWidget { children: [ MinimizeWindowButton( colors: WindowButtonColors( - iconNormal: SystemTheme.accentColor.accent, + iconNormal: + AdaptiveTheme.of(context).theme.colorScheme.secondary, iconMouseOver: Colors.green, ), ), MaximizeWindowButton( colors: WindowButtonColors( - iconNormal: SystemTheme.accentColor.accent, + iconNormal: + AdaptiveTheme.of(context).theme.colorScheme.secondary, iconMouseOver: Colors.orange, ), ), CloseWindowButton( colors: WindowButtonColors( - iconNormal: SystemTheme.accentColor.accent, + iconNormal: + AdaptiveTheme.of(context).theme.colorScheme.secondary, iconMouseOver: Colors.red, ), ), From 26de8dd0b759cffaa6823ef7e5e9adc1782971c4 Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:21:53 +0200 Subject: [PATCH 08/13] Moved snackbar.dart to core --- app/lib/backend/providers/model.dart | 7 +++---- app/lib/{frontend/helpers => core}/snackbar.dart | 0 app/lib/core/update.dart | 2 +- app/lib/frontend/pages/dashboard/models.dart | 2 +- app/lib/frontend/pages/dashboard/sessions.dart | 3 +-- app/lib/frontend/pages/dashboard/settings.dart | 2 +- app/lib/frontend/screens/dashboard.dart | 2 +- app/lib/frontend/screens/onboarding.dart | 2 +- app/lib/frontend/widgets/chat_example_questions.dart | 3 +-- app/lib/frontend/widgets/chat_input_field.dart | 3 +-- app/lib/frontend/widgets/chat_message.dart | 2 +- app/lib/frontend/widgets/chat_toolbar.dart | 3 +-- app/lib/frontend/widgets/markdown_body.dart | 3 +-- app/lib/frontend/widgets/markdown_code_wrapper.dart | 2 +- 14 files changed, 15 insertions(+), 21 deletions(-) rename app/lib/{frontend/helpers => core}/snackbar.dart (100%) diff --git a/app/lib/backend/providers/model.dart b/app/lib/backend/providers/model.dart index 614b52d..874944d 100644 --- a/app/lib/backend/providers/model.dart +++ b/app/lib/backend/providers/model.dart @@ -4,7 +4,6 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; - import 'package:http/http.dart' as http; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:open_local_ui/backend/models/model.dart'; @@ -13,7 +12,7 @@ import 'package:open_local_ui/backend/providers/model_settings.dart'; import 'package:open_local_ui/constants/flutter.dart'; import 'package:open_local_ui/core/http.dart'; import 'package:open_local_ui/core/logger.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:windows_taskbar/windows_taskbar.dart'; enum ModelProviderStatus { @@ -182,7 +181,7 @@ class ModelProvider extends ChangeNotifier { WindowsTaskbar.resetThumbnailToolbar(); WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); } - + SnackBarHelpers.showSnackBar( // ignore: use_build_context_synchronously AppLocalizations.of(scaffoldMessengerKey.currentState!.context) @@ -393,7 +392,7 @@ class ModelProvider extends ChangeNotifier { WindowsTaskbar.resetThumbnailToolbar(); WindowsTaskbar.setProgressMode(TaskbarProgressMode.noProgress); } - + SnackBarHelpers.showSnackBar( // ignore: use_build_context_synchronously AppLocalizations.of(scaffoldMessengerKey.currentState!.context) diff --git a/app/lib/frontend/helpers/snackbar.dart b/app/lib/core/snackbar.dart similarity index 100% rename from app/lib/frontend/helpers/snackbar.dart rename to app/lib/core/snackbar.dart diff --git a/app/lib/core/update.dart b/app/lib/core/update.dart index e68b969..6117063 100644 --- a/app/lib/core/update.dart +++ b/app/lib/core/update.dart @@ -7,7 +7,7 @@ import 'package:open_local_ui/core/github.dart'; import 'package:open_local_ui/core/logger.dart'; import 'package:open_local_ui/core/process.dart'; import 'package:open_local_ui/env.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/app/lib/frontend/pages/dashboard/models.dart b/app/lib/frontend/pages/dashboard/models.dart index b7a1eaf..688160c 100644 --- a/app/lib/frontend/pages/dashboard/models.dart +++ b/app/lib/frontend/pages/dashboard/models.dart @@ -15,7 +15,7 @@ import 'package:open_local_ui/frontend/dialogs/model_details.dart'; import 'package:open_local_ui/frontend/dialogs/model_settings.dart'; import 'package:open_local_ui/frontend/dialogs/pull_model.dart'; import 'package:open_local_ui/frontend/dialogs/push_model.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:open_local_ui/frontend/screens/dashboard.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; diff --git a/app/lib/frontend/pages/dashboard/sessions.dart b/app/lib/frontend/pages/dashboard/sessions.dart index ba734a9..d3552ad 100644 --- a/app/lib/frontend/pages/dashboard/sessions.dart +++ b/app/lib/frontend/pages/dashboard/sessions.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; - import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:gap/gap.dart'; @@ -10,7 +9,7 @@ import 'package:open_local_ui/backend/models/chat_session.dart'; import 'package:open_local_ui/backend/providers/chat.dart'; import 'package:open_local_ui/core/formatters.dart'; import 'package:open_local_ui/frontend/dialogs/confirmation.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:open_local_ui/frontend/screens/dashboard.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; diff --git a/app/lib/frontend/pages/dashboard/settings.dart b/app/lib/frontend/pages/dashboard/settings.dart index 3b8cd37..c9f8a1b 100644 --- a/app/lib/frontend/pages/dashboard/settings.dart +++ b/app/lib/frontend/pages/dashboard/settings.dart @@ -9,7 +9,7 @@ import 'package:open_local_ui/backend/providers/chat.dart'; import 'package:open_local_ui/backend/providers/locale.dart'; import 'package:open_local_ui/core/color.dart'; import 'package:open_local_ui/frontend/dialogs/color_picker.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:system_theme/system_theme.dart'; diff --git a/app/lib/frontend/screens/dashboard.dart b/app/lib/frontend/screens/dashboard.dart index 02ce5da..cca8265 100644 --- a/app/lib/frontend/screens/dashboard.dart +++ b/app/lib/frontend/screens/dashboard.dart @@ -14,7 +14,7 @@ import 'package:open_local_ui/core/github.dart'; import 'package:open_local_ui/core/logger.dart'; import 'package:open_local_ui/core/update.dart'; import 'package:open_local_ui/frontend/dialogs/update.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:open_local_ui/frontend/pages/dashboard/about.dart'; import 'package:open_local_ui/frontend/pages/dashboard/chat.dart'; import 'package:open_local_ui/frontend/pages/dashboard/models.dart'; diff --git a/app/lib/frontend/screens/onboarding.dart b/app/lib/frontend/screens/onboarding.dart index b522c75..e564ae3 100644 --- a/app/lib/frontend/screens/onboarding.dart +++ b/app/lib/frontend/screens/onboarding.dart @@ -16,7 +16,7 @@ import 'package:open_local_ui/frontend/components/typewriter_text.dart'; import 'package:open_local_ui/core/color.dart'; import 'package:open_local_ui/core/process.dart'; import 'package:open_local_ui/frontend/dialogs/color_picker.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:open_local_ui/frontend/screens/dashboard.dart'; import 'package:open_local_ui/frontend/widgets/preference_selector.dart'; import 'package:open_local_ui/frontend/components/window_management_bar.dart'; diff --git a/app/lib/frontend/widgets/chat_example_questions.dart b/app/lib/frontend/widgets/chat_example_questions.dart index 930e45e..9e726c2 100644 --- a/app/lib/frontend/widgets/chat_example_questions.dart +++ b/app/lib/frontend/widgets/chat_example_questions.dart @@ -2,14 +2,13 @@ import 'dart:math'; import 'package:flutter/material.dart'; - import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:gap/gap.dart'; import 'package:open_local_ui/backend/providers/chat.dart'; import 'package:open_local_ui/backend/providers/model.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:provider/provider.dart'; import 'package:unicons/unicons.dart'; diff --git a/app/lib/frontend/widgets/chat_input_field.dart b/app/lib/frontend/widgets/chat_input_field.dart index 8cb7ec1..2614f3a 100644 --- a/app/lib/frontend/widgets/chat_input_field.dart +++ b/app/lib/frontend/widgets/chat_input_field.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; - import 'package:flutter_animate/flutter_animate.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:gap/gap.dart'; @@ -9,7 +8,7 @@ import 'package:open_local_ui/backend/providers/chat.dart'; import 'package:open_local_ui/backend/providers/model.dart'; import 'package:open_local_ui/core/image.dart'; import 'package:open_local_ui/frontend/dialogs/attachments_dropzone.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:provider/provider.dart'; import 'package:unicons/unicons.dart'; diff --git a/app/lib/frontend/widgets/chat_message.dart b/app/lib/frontend/widgets/chat_message.dart index 581fdf5..7291672 100644 --- a/app/lib/frontend/widgets/chat_message.dart +++ b/app/lib/frontend/widgets/chat_message.dart @@ -12,7 +12,7 @@ import 'package:gap/gap.dart'; import 'package:open_local_ui/backend/models/chat_message.dart'; import 'package:open_local_ui/backend/providers/chat.dart'; import 'package:open_local_ui/core/formatters.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:open_local_ui/frontend/widgets/markdown_body.dart'; import 'package:open_local_ui/frontend/widgets/tts_player.dart'; import 'package:provider/provider.dart'; diff --git a/app/lib/frontend/widgets/chat_toolbar.dart b/app/lib/frontend/widgets/chat_toolbar.dart index 88b0b0f..bfba88a 100644 --- a/app/lib/frontend/widgets/chat_toolbar.dart +++ b/app/lib/frontend/widgets/chat_toolbar.dart @@ -1,12 +1,11 @@ import 'package:flutter/material.dart'; - import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:gap/gap.dart'; import 'package:open_local_ui/backend/providers/chat.dart'; import 'package:open_local_ui/backend/providers/model.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:provider/provider.dart'; import 'package:unicons/unicons.dart'; diff --git a/app/lib/frontend/widgets/markdown_body.dart b/app/lib/frontend/widgets/markdown_body.dart index 9c49532..6327c8d 100644 --- a/app/lib/frontend/widgets/markdown_body.dart +++ b/app/lib/frontend/widgets/markdown_body.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; - import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; // ignore: depend_on_referenced_packages @@ -9,7 +8,7 @@ import 'package:flutter_highlight/themes/atom-one-dark.dart'; import 'package:flutter_highlight/themes/atom-one-light.dart'; import 'package:markdown_widget/markdown_widget.dart'; import 'package:open_local_ui/constants/style.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:open_local_ui/frontend/widgets/markdown_code_wrapper.dart'; import 'package:url_launcher/url_launcher.dart'; diff --git a/app/lib/frontend/widgets/markdown_code_wrapper.dart b/app/lib/frontend/widgets/markdown_code_wrapper.dart index ca570d3..d26beb9 100644 --- a/app/lib/frontend/widgets/markdown_code_wrapper.dart +++ b/app/lib/frontend/widgets/markdown_code_wrapper.dart @@ -9,7 +9,7 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:gap/gap.dart'; import 'package:open_local_ui/core/asset.dart'; -import 'package:open_local_ui/frontend/helpers/snackbar.dart'; +import 'package:open_local_ui/core/snackbar.dart'; import 'package:unicons/unicons.dart'; Map languageToAsset = { From 1d02fd81c6cce67d3320181c46f6d759b230d73f Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Thu, 22 Aug 2024 10:56:38 +0200 Subject: [PATCH 09/13] Add battery_plus package for power delivery warning --- app/assets/l10n/intl_en.arb | 2 ++ app/lib/frontend/screens/dashboard.dart | 30 +++++++++++++++++++ .../Flutter/GeneratedPluginRegistrant.swift | 2 ++ app/pubspec.lock | 24 +++++++++++++++ app/pubspec.yaml | 1 + .../flutter/generated_plugin_registrant.cc | 3 ++ app/windows/flutter/generated_plugins.cmake | 1 + 7 files changed, 63 insertions(+) diff --git a/app/assets/l10n/intl_en.arb b/app/assets/l10n/intl_en.arb index 875a67a..25e2109 100644 --- a/app/assets/l10n/intl_en.arb +++ b/app/assets/l10n/intl_en.arb @@ -1,5 +1,7 @@ { "@@locale": "en", + "deviceUnpluggedSnackBar": "Device unplugged. Power delivery reduced.", + "devicePluggedInSnackBar": "Device plugged in. Power delivery increased.", "abortModelRemovalSnackBar": "Abort model removal", "abortSessionRemovalSnackBar": "Abort session removal", "aboutPageContributorsTitle": "Contributors", diff --git a/app/lib/frontend/screens/dashboard.dart b/app/lib/frontend/screens/dashboard.dart index cca8265..f210e13 100644 --- a/app/lib/frontend/screens/dashboard.dart +++ b/app/lib/frontend/screens/dashboard.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:battery_plus/battery_plus.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -47,6 +48,7 @@ class _DashboardScreenState extends State { WidgetsBinding.instance.addPostFrameCallback((_) async { _checkForUpdates(); + _registerBatteryCallback(); }); } @@ -57,6 +59,34 @@ class _DashboardScreenState extends State { super.dispose(); } + void _registerBatteryCallback() { + final battery = Battery(); + + battery.onBatteryStateChanged.listen((BatteryState state) { + switch (state) { + case BatteryState.discharging: + SnackBarHelpers.showSnackBar( + AppLocalizations.of(context).snackBarWarningTitle, + AppLocalizations.of(context).deviceUnpluggedSnackBar, + SnackbarContentType.warning, + ); + logger.i('Battery charging'); + break; + case BatteryState.charging: + SnackBarHelpers.showSnackBar( + AppLocalizations.of(context).snackBarSuccessTitle, + AppLocalizations.of(context).devicePluggedInSnackBar, + SnackbarContentType.success, + ); + logger.i('Battery discharging'); + break; + default: + logger.i('Battery state: $state'); + break; + } + }); + } + void _checkForUpdates() { UpdateHelper.isAppUpdateAvailable().then( (updateAvailable) { diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift index 16f81a3..ba48a82 100644 --- a/app/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -7,6 +7,7 @@ import Foundation import app_links import audioplayers_darwin +import battery_plus import bitsdojo_window_macos import device_info_plus import irondash_engine_context @@ -23,6 +24,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin")) AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) + BatteryPlusMacosPlugin.register(with: registry.registrar(forPlugin: "BatteryPlusMacosPlugin")) BitsdojoWindowPlugin.register(with: registry.registrar(forPlugin: "BitsdojoWindowPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) IrondashEngineContextPlugin.register(with: registry.registrar(forPlugin: "IrondashEngineContextPlugin")) diff --git a/app/pubspec.lock b/app/pubspec.lock index b587afb..8bac7a4 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -121,6 +121,22 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.0" + battery_plus: + dependency: "direct main" + description: + name: battery_plus + sha256: ccc1322fee1153a0f89e663e0eac2f64d659da506454cf24dcad75eb08ae138b + url: "https://pub.dev" + source: hosted + version: "6.0.2" + battery_plus_platform_interface: + dependency: transitive + description: + name: battery_plus_platform_interface + sha256: e8342c0f32de4b1dfd0223114b6785e48e579bfc398da9471c9179b907fa4910 + url: "https://pub.dev" + source: hosted + version: "2.0.1" bitsdojo_window: dependency: "direct main" description: @@ -361,6 +377,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + dbus: + dependency: transitive + description: + name: dbus + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" + url: "https://pub.dev" + source: hosted + version: "0.7.10" device_info_plus: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index f0867a4..04e7a1c 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -86,6 +86,7 @@ dependencies: language_code: ^0.5.3+2 system_info2: ^4.0.0 gpu_info: ^0.0.3 + battery_plus: ^6.0.2 # Environment Variables envied: ^0.5.4+1 diff --git a/app/windows/flutter/generated_plugin_registrant.cc b/app/windows/flutter/generated_plugin_registrant.cc index 94f2b5f..58fbd3c 100644 --- a/app/windows/flutter/generated_plugin_registrant.cc +++ b/app/windows/flutter/generated_plugin_registrant.cc @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("AppLinksPluginCApi")); AudioplayersWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin")); + BatteryPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("BatteryPlusWindowsPlugin")); BitsdojoWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); GpuInfoPluginCApiRegisterWithRegistrar( diff --git a/app/windows/flutter/generated_plugins.cmake b/app/windows/flutter/generated_plugins.cmake index 1f41cd0..de294b5 100644 --- a/app/windows/flutter/generated_plugins.cmake +++ b/app/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST app_links audioplayers_windows + battery_plus bitsdojo_window_windows gpu_info irondash_engine_context From 3b1ffd73435b6bd8a348e7180f4fbe1bf4a38bb2 Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Thu, 22 Aug 2024 11:09:54 +0200 Subject: [PATCH 10/13] Add an animated splash screen --- app/assets/graphics/logos/open_local_ui.svg | 3 ++ app/lib/frontend/screens/onboarding.dart | 2 +- app/lib/frontend/screens/splash.dart | 33 ++++++++++++++++++++ app/lib/main.dart | 13 ++------ app/pubspec.lock | 34 ++++++++++----------- app/pubspec.yaml | 3 +- 6 files changed, 59 insertions(+), 29 deletions(-) create mode 100644 app/assets/graphics/logos/open_local_ui.svg create mode 100644 app/lib/frontend/screens/splash.dart diff --git a/app/assets/graphics/logos/open_local_ui.svg b/app/assets/graphics/logos/open_local_ui.svg new file mode 100644 index 0000000..ba0162e --- /dev/null +++ b/app/assets/graphics/logos/open_local_ui.svg @@ -0,0 +1,3 @@ + + + diff --git a/app/lib/frontend/screens/onboarding.dart b/app/lib/frontend/screens/onboarding.dart index e564ae3..adf4472 100644 --- a/app/lib/frontend/screens/onboarding.dart +++ b/app/lib/frontend/screens/onboarding.dart @@ -54,7 +54,7 @@ class _OnboardingScreenState extends State { title: AppLocalizations.of(context).setupPageWelcomeSlideTitle, bodyWidget: TypewriterTextComponent( text: AppLocalizations.of(context).setupPageWelcomeSlideText, - duration: 1500.ms, + duration: 3000.ms, ), decoration: const PageDecoration( titleTextStyle: TextStyle( diff --git a/app/lib/frontend/screens/splash.dart b/app/lib/frontend/screens/splash.dart new file mode 100644 index 0000000..749357b --- /dev/null +++ b/app/lib/frontend/screens/splash.dart @@ -0,0 +1,33 @@ +import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:flutter/material.dart'; + +import 'package:animated_splash_screen/animated_splash_screen.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:open_local_ui/frontend/screens/dashboard.dart'; +import 'package:open_local_ui/frontend/screens/onboarding.dart'; +import 'package:page_transition/page_transition.dart'; + +class SplashScreen extends StatelessWidget { + final bool userOnboarded; + + const SplashScreen({Key? key, required this.userOnboarded}) : super(key: key); + + @override + Widget build(BuildContext context) { + return AnimatedSplashScreen( + splash: SvgPicture.asset( + 'assets/graphics/logos/open_local_ui.svg', + width: 512, + // ignore: deprecated_member_use + color: + AdaptiveTheme.of(context).mode.isDark ? Colors.white : Colors.black, + ), + nextScreen: + userOnboarded ? const DashboardScreen() : const OnboardingScreen(), + backgroundColor: AdaptiveTheme.of(context).theme.primaryColor, + splashTransition: SplashTransition.fadeTransition, + pageTransitionType: PageTransitionType.theme, + duration: 1500, + ); + } +} diff --git a/app/lib/main.dart b/app/lib/main.dart index aa9ee15..b09fd41 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -7,7 +7,6 @@ import 'package:feedback/feedback.dart'; import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'package:open_local_ui/backend/databases/chat_sessions.dart'; import 'package:open_local_ui/backend/providers/chat.dart'; import 'package:open_local_ui/backend/providers/locale.dart'; @@ -19,8 +18,7 @@ import 'package:open_local_ui/core/asset.dart'; import 'package:open_local_ui/core/color.dart'; import 'package:open_local_ui/core/logger.dart'; import 'package:open_local_ui/env.dart'; -import 'package:open_local_ui/frontend/screens/dashboard.dart'; -import 'package:open_local_ui/frontend/screens/onboarding.dart'; +import 'package:open_local_ui/frontend/screens/splash.dart'; import 'package:provider/provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:supabase_flutter/supabase_flutter.dart'; @@ -90,8 +88,7 @@ void _preloadAssets() async { } void main() async { - WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); - FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); + WidgetsFlutterBinding.ensureInitialized(); // Internal services @@ -141,8 +138,6 @@ void main() async { prefs.setBool('userOnboarded', true); } - FlutterNativeSplash.remove(); - // Run app runApp( @@ -240,9 +235,7 @@ class _MyAppState extends State { GlobalWidgetsLocalizations.delegate, GlobalCupertinoLocalizations.delegate, ], - home: widget.userOnboarded - ? const DashboardScreen() - : const OnboardingScreen(), + home: SplashScreen(userOnboarded: widget.userOnboarded), debugShowCheckedModeBanner: kDebugMode, ), ); diff --git a/app/pubspec.lock b/app/pubspec.lock index 8bac7a4..654f36d 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -25,14 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.13.0" - ansicolor: - dependency: transitive + animated_splash_screen: + dependency: "direct main" description: - name: ansicolor - sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + name: animated_splash_screen + sha256: f45634db6ec4e8cf034c53e03f3bd83898a16fe3c9286bf5510b6831dfcf2124 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "1.3.0" app_links: dependency: transitive description: @@ -619,14 +619,6 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_native_splash: - dependency: "direct main" - description: - name: flutter_native_splash - sha256: edf39bcf4d74aca1eb2c1e43c3e445fd9f494013df7f0da752fefe72020eedc0 - url: "https://pub.dev" - source: hosted - version: "2.4.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1141,6 +1133,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + page_transition: + dependency: transitive + description: + name: page_transition + sha256: dee976b1f23de9bbef5cd512fe567e9f6278caee11f5eaca9a2115c19dc49ef6 + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: "direct main" description: @@ -1642,14 +1642,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" - universal_io: + upower: dependency: transitive description: - name: universal_io - sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + name: upower + sha256: cf042403154751180affa1d15614db7fa50234bc2373cd21c3db666c38543ebf url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "0.7.0" url_launcher: dependency: "direct main" description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 04e7a1c..67af11d 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -42,7 +42,7 @@ dependencies: flutter_svg: ^2.0.10+1 # Splash Screen - flutter_native_splash: ^2.4.0 + animated_splash_screen: ^1.3.0 # Window Management bitsdojo_window: ^0.1.6 @@ -126,6 +126,7 @@ flutter: # Model Metadata - assets/metadata/ollama_models.json # Logos + - assets/graphics/logos/open_local_ui.svg - assets/graphics/logos/flutter.svg - assets/graphics/logos/langchain.svg - assets/graphics/logos/supabase.svg From 7d79d25f925944607f4d63304ab0f517a3c631ed Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Thu, 22 Aug 2024 18:47:12 +0200 Subject: [PATCH 11/13] Fix warnings, runtime exceptions and update dependencies --- app/lib/core/asset.dart | 69 ++-- app/lib/core/logger.dart | 2 +- .../frontend/components/rive_animation.dart | 17 +- .../frontend/components/typewriter_text.dart | 2 +- app/lib/frontend/dialogs/color_picker.dart | 2 +- .../frontend/pages/dashboard/settings.dart | 15 +- app/lib/frontend/screens/dashboard.dart | 20 +- app/lib/frontend/screens/onboarding.dart | 22 +- app/lib/frontend/screens/splash.dart | 2 +- .../widgets/markdown_code_wrapper.dart | 5 +- .../frontend/widgets/preference_selector.dart | 2 +- app/lib/main.dart | 6 +- app/pubspec.lock | 316 ++++++++++-------- app/pubspec.yaml | 8 +- 14 files changed, 276 insertions(+), 212 deletions(-) diff --git a/app/lib/core/asset.dart b/app/lib/core/asset.dart index a5b0c47..c6d97b8 100644 --- a/app/lib/core/asset.dart +++ b/app/lib/core/asset.dart @@ -1,9 +1,10 @@ import 'dart:convert'; import 'package:flutter/services.dart'; + import 'package:open_local_ui/core/http.dart'; import 'package:open_local_ui/core/logger.dart'; - +import 'package:rive/rive.dart'; import 'package:shared_preferences/shared_preferences.dart'; enum AssetSource { @@ -15,6 +16,7 @@ enum AssetType { raw, json, binary, + rivefile, } /// Manages the assets used in the application. @@ -25,7 +27,7 @@ enum AssetType { /// /// NOTE: The pool is most effective with small sized and frequently accessed assets. class AssetManager { - static final Map _assetRegistry = {}; + static final Map _assetRegistry = {}; /// Wapper around [SharedPreferences] to save a key-value pair to the device's preferences. /// @@ -47,17 +49,16 @@ class AssetManager { static Future getFromPreferences(String key) async { final prefs = await SharedPreferences.getInstance(); logger.d('Retrieved from preferences: $key'); - switch (T) { - case String: - return prefs.getString(key) as T?; - case int: - return prefs.getInt(key) as T?; - case double: - return prefs.getDouble(key) as T?; - case bool: - return prefs.getBool(key) as T?; - default: - throw Exception('Invalid preference type'); + if (T is String) { + return prefs.getString(key) as T?; + } else if (T is int) { + return prefs.getInt(key) as T?; + } else if (T is double) { + return prefs.getDouble(key) as T?; + } else if (T is bool) { + return prefs.getBool(key) as T?; + } else { + throw Exception('Invalid value type'); } } @@ -67,8 +68,7 @@ class AssetManager { /// /// The method returns a [Map] that represents the asset in JSON format. static Map _getAssetAsJson(String key) { - final assetContent = _getRawAsset(key); - return jsonDecode(assetContent!); + return _getRawAsset(key) as Map; } /// Retrieves an asset from the asset pool in the binary format. @@ -76,9 +76,8 @@ class AssetManager { /// The [key] parameter should be a string representing the path of the asset to be retrieved. /// /// The method returns a [Uint8List] that represents the asset in binary format. - static Uint8List _getAssetAsBytes(String key) { - final assetContent = _getRawAsset(key); - return Uint8List.fromList(assetContent!.codeUnits); + static ByteData _getAssetAsBytes(String key) { + return _getRawAsset(key) as ByteData; } /// Retrieves an asset from the asset pool in plain text format. @@ -86,11 +85,20 @@ class AssetManager { /// The [key] parameter should be a string representing the path of the asset to be retrieved. /// /// The method returns a [String] that represents the asset in plain text format. - static String? _getRawAsset(String key) { + static dynamic _getRawAsset(String key) { logger.d('Retrieved asset: $key'); return _assetRegistry[key]; } + /// Retrieves an asset from the asset pool in the RiveFile format. + /// + /// The [key] parameter should be a string representing the path of the asset to be retrieved. + /// + /// The method returns a [RiveFile] that represents the asset in RiveFile format. + static RiveFile _getAssetAsRiveFile(String key) { + return _getRawAsset(key) as RiveFile; + } + static dynamic getAsset( String key, { required AssetType type, @@ -102,6 +110,8 @@ class AssetManager { return _getAssetAsJson(key); case AssetType.binary: return _getAssetAsBytes(key); + case AssetType.rivefile: + return _getAssetAsRiveFile(key); default: throw Exception('Invalid asset type'); } @@ -124,18 +134,33 @@ class AssetManager { /// The [forceReload] parameter should be a boolean value indicating if the asset should be reloaded if it already exists in the pool. /// /// The method returns a [Future] that resolves to the asset content in plain text format. - static Future loadAsset( + static Future loadAsset( String key, { required AssetSource source, AssetType type = AssetType.raw, bool forceReload = false, }) async { if (!isAssetLoaded(key) || (isAssetLoaded(key) && forceReload)) { - late String assetContent; + late dynamic assetContent; switch (source) { case AssetSource.local: - assetContent = await rootBundle.loadString(key); + switch (type) { + case AssetType.raw: + assetContent = await rootBundle.loadString(key); + break; + case AssetType.json: + assetContent = jsonDecode(await rootBundle.loadString(key)); + break; + case AssetType.binary: + assetContent = await rootBundle.load(key); + break; + case AssetType.rivefile: + assetContent = RiveFile.import(await rootBundle.load(key)); + break; + default: + throw Exception('Invalid asset type'); + } break; case AssetSource.remote: assetContent = await HTTPHelpers.get(key).then( diff --git a/app/lib/core/logger.dart b/app/lib/core/logger.dart index 358fb6d..b9cf44a 100644 --- a/app/lib/core/logger.dart +++ b/app/lib/core/logger.dart @@ -45,7 +45,7 @@ Future initLogger() async { printer: PrettyPrinter( lineLength: 80, printEmojis: true, - printTime: true, + dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, ), output: logOutput, level: logLevel, diff --git a/app/lib/frontend/components/rive_animation.dart b/app/lib/frontend/components/rive_animation.dart index e3c1fc8..387e5e2 100644 --- a/app/lib/frontend/components/rive_animation.dart +++ b/app/lib/frontend/components/rive_animation.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; @@ -41,15 +40,13 @@ class _RiveAnimationComponentState extends State { } Future _loadRiveAnimation(String filename) async { - if (AssetManager.isAssetLoaded(filename)) { - final buffer = AssetManager.getAsset(filename, type: AssetType.binary); - final bytes = ByteData.view(buffer.buffer); - return RiveFile.import(bytes); - } else { - final bytes = await rootBundle.load(filename); - await RiveFile.initialize(); - return RiveFile.import(bytes); - } + final animation = await AssetManager.loadAsset( + filename, + source: AssetSource.local, + type: AssetType.rivefile, + ); + + return animation as RiveFile; } @override diff --git a/app/lib/frontend/components/typewriter_text.dart b/app/lib/frontend/components/typewriter_text.dart index c495240..60af6e5 100644 --- a/app/lib/frontend/components/typewriter_text.dart +++ b/app/lib/frontend/components/typewriter_text.dart @@ -11,7 +11,7 @@ class TypewriterTextComponent extends StatefulWidget { }); @override - _TypewriterTextComponentState createState() => + State createState() => _TypewriterTextComponentState(); } diff --git a/app/lib/frontend/dialogs/color_picker.dart b/app/lib/frontend/dialogs/color_picker.dart index db54c30..21f3c2a 100644 --- a/app/lib/frontend/dialogs/color_picker.dart +++ b/app/lib/frontend/dialogs/color_picker.dart @@ -12,7 +12,7 @@ class ColorPickerDialog extends StatefulWidget { }); @override - _ColorPickerDialogState createState() => _ColorPickerDialogState(); + State createState() => _ColorPickerDialogState(); } class _ColorPickerDialogState extends State { diff --git a/app/lib/frontend/pages/dashboard/settings.dart b/app/lib/frontend/pages/dashboard/settings.dart index c9f8a1b..0a8bdfb 100644 --- a/app/lib/frontend/pages/dashboard/settings.dart +++ b/app/lib/frontend/pages/dashboard/settings.dart @@ -63,6 +63,8 @@ class ThemeSettings extends StatefulWidget { } class _ThemeSettingsState extends State { + final _key = GlobalKey<_ThemeSettingsState>(); + Future _isAccentSynced() async { final prefs = await SharedPreferences.getInstance(); return prefs.getBool('sync_accent_color') ?? false; @@ -171,13 +173,13 @@ class _ThemeSettingsState extends State { const Gap(8.0), Text( AppLocalizations.of(context).settingsPageAccentColorLabel, - style: TextStyle(fontSize: 16.0), + style: const TextStyle(fontSize: 16.0), ), const Gap(8.0), GestureDetector( onTap: () async { showColorPickerDialog( - context, + _key.currentContext!, await _getAccent(), ).then( (color) async { @@ -187,7 +189,7 @@ class _ThemeSettingsState extends State { if ((prefs.getBool('sync_accent_color') ?? false) == false) { - _setAccent(context, color); + _setAccent(_key.currentContext!, color); } else { setState(() {}); } @@ -222,7 +224,7 @@ class _ThemeSettingsState extends State { const Gap(8.0), Text( AppLocalizations.of(context).settingsPageSyncAccentColorLabel, - style: TextStyle(fontSize: 16.0), + style: const TextStyle(fontSize: 16.0), ), const Gap(8.0), FutureBuilder( @@ -242,13 +244,14 @@ class _ThemeSettingsState extends State { if (value) { await prefs.setBool('sync_accent_color', true); - _setAccent(context, SystemTheme.accentColor.accent); + _setAccent(_key.currentContext!, + SystemTheme.accentColor.accent); } else { final savedColorCode = prefs.getString('accent_color'); prefs.setBool('sync_accent_color', false); _setAccent( - context, + _key.currentContext!, ColorHelpers.colorFromHex( savedColorCode ?? Colors.cyan.hex, ), diff --git a/app/lib/frontend/screens/dashboard.dart b/app/lib/frontend/screens/dashboard.dart index f210e13..ed6d176 100644 --- a/app/lib/frontend/screens/dashboard.dart +++ b/app/lib/frontend/screens/dashboard.dart @@ -37,10 +37,10 @@ class DashboardScreen extends StatefulWidget { } class _DashboardScreenState extends State { - final PageController _pageController = PageController(); - final OverlayPortalController _overlayPortalController = - OverlayPortalController(); - final GlobalKey _buttonKey = GlobalKey(); + final _key = GlobalKey<_DashboardScreenState>(); + final _buttonKey = GlobalKey(); + final _pageController = PageController(); + final _overlayPortalController = OverlayPortalController(); @override void initState() { @@ -66,16 +66,16 @@ class _DashboardScreenState extends State { switch (state) { case BatteryState.discharging: SnackBarHelpers.showSnackBar( - AppLocalizations.of(context).snackBarWarningTitle, - AppLocalizations.of(context).deviceUnpluggedSnackBar, + AppLocalizations.of(_key.currentContext!).snackBarWarningTitle, + AppLocalizations.of(_key.currentContext!).deviceUnpluggedSnackBar, SnackbarContentType.warning, ); logger.i('Battery charging'); break; case BatteryState.charging: SnackBarHelpers.showSnackBar( - AppLocalizations.of(context).snackBarSuccessTitle, - AppLocalizations.of(context).devicePluggedInSnackBar, + AppLocalizations.of(_key.currentContext!).snackBarSuccessTitle, + AppLocalizations.of(_key.currentContext!).devicePluggedInSnackBar, SnackbarContentType.success, ); logger.i('Battery discharging'); @@ -92,8 +92,8 @@ class _DashboardScreenState extends State { (updateAvailable) { if (updateAvailable) { SnackBarHelpers.showSnackBar( - AppLocalizations.of(context).snackBarUpdateTitle, - AppLocalizations.of(context) + AppLocalizations.of(_key.currentContext!).snackBarUpdateTitle, + AppLocalizations.of(_key.currentContext!) .clickToDownloadLatestAppVersionSnackBar, SnackbarContentType.info, onTap: () => showUpdateDialog( diff --git a/app/lib/frontend/screens/onboarding.dart b/app/lib/frontend/screens/onboarding.dart index adf4472..eecdc68 100644 --- a/app/lib/frontend/screens/onboarding.dart +++ b/app/lib/frontend/screens/onboarding.dart @@ -407,10 +407,11 @@ class SystemAnalysisPage extends StatefulWidget { const SystemAnalysisPage({super.key}); @override - _SystemAnalysisPageState createState() => _SystemAnalysisPageState(); + State createState() => _SystemAnalysisPageState(); } class _SystemAnalysisPageState extends State { + final _key = GlobalKey<_SystemAnalysisPageState>(); List? _gpusInfo; Future> _getGpusInfo() async { @@ -446,7 +447,7 @@ class _SystemAnalysisPageState extends State { final gpuName = bestGpu?.deviceName ?? "Unknown GPU"; final gpuMemory = bestGpu?.memoryAmount ?? 0; - return AppLocalizations.of(context).systemInfo( + return AppLocalizations.of(_key.currentContext!).systemInfo( osName, osVersion, cpuName, @@ -490,10 +491,12 @@ class ThemeSelectionPage extends StatefulWidget { const ThemeSelectionPage({super.key}); @override - _ThemeSelectionPageState createState() => _ThemeSelectionPageState(); + State createState() => _ThemeSelectionPageState(); } class _ThemeSelectionPageState extends State { + final GlobalKey<_ThemeSelectionPageState> _key = GlobalKey(); + Future _isAccentSynced() async { final prefs = await SharedPreferences.getInstance(); return prefs.getBool('sync_accent_color') ?? false; @@ -602,13 +605,13 @@ class _ThemeSelectionPageState extends State { const Gap(8.0), Text( AppLocalizations.of(context).settingsPageAccentColorLabel, - style: TextStyle(fontSize: 16.0), + style: const TextStyle(fontSize: 16.0), ), const Gap(8.0), GestureDetector( onTap: () async { showColorPickerDialog( - context, + _key.currentContext!, await _getAccent(), ).then( (color) async { @@ -618,7 +621,7 @@ class _ThemeSelectionPageState extends State { if ((prefs.getBool('sync_accent_color') ?? false) == false) { - _setAccent(context, color); + _setAccent(_key.currentContext!, color); } else { setState(() {}); } @@ -653,7 +656,7 @@ class _ThemeSelectionPageState extends State { const Gap(8.0), Text( AppLocalizations.of(context).settingsPageSyncAccentColorLabel, - style: TextStyle(fontSize: 16.0), + style: const TextStyle(fontSize: 16.0), ), const Gap(8.0), FutureBuilder( @@ -673,13 +676,14 @@ class _ThemeSelectionPageState extends State { if (value) { await prefs.setBool('sync_accent_color', true); - _setAccent(context, SystemTheme.accentColor.accent); + _setAccent(_key.currentContext!, + SystemTheme.accentColor.accent); } else { final savedColorCode = prefs.getString('accent_color'); prefs.setBool('sync_accent_color', false); _setAccent( - context, + _key.currentContext!, ColorHelpers.colorFromHex( savedColorCode ?? Colors.cyan.hex, ), diff --git a/app/lib/frontend/screens/splash.dart b/app/lib/frontend/screens/splash.dart index 749357b..ff82a82 100644 --- a/app/lib/frontend/screens/splash.dart +++ b/app/lib/frontend/screens/splash.dart @@ -10,7 +10,7 @@ import 'package:page_transition/page_transition.dart'; class SplashScreen extends StatelessWidget { final bool userOnboarded; - const SplashScreen({Key? key, required this.userOnboarded}) : super(key: key); + const SplashScreen({super.key, required this.userOnboarded}); @override Widget build(BuildContext context) { diff --git a/app/lib/frontend/widgets/markdown_code_wrapper.dart b/app/lib/frontend/widgets/markdown_code_wrapper.dart index 9d275c3..dcb21d4 100644 --- a/app/lib/frontend/widgets/markdown_code_wrapper.dart +++ b/app/lib/frontend/widgets/markdown_code_wrapper.dart @@ -83,6 +83,7 @@ class MarkdownCodeWrapperWidget extends StatefulWidget { } class _CodeWrapperState extends State { + final _key = GlobalKey<_CodeWrapperState>(); bool _isCopied = false; bool _isSaved = false; @@ -116,13 +117,13 @@ class _CodeWrapperState extends State { await file.writeAsString(widget.text); SnackBarHelpers.showSnackBar( - AppLocalizations.of(context).snackBarSuccessTitle, + AppLocalizations.of(_key.currentContext!).snackBarSuccessTitle, 'File saved at: ${file.path}', SnackbarContentType.success, ); } else { SnackBarHelpers.showSnackBar( - AppLocalizations.of(context).snackBarErrorTitle, + AppLocalizations.of(_key.currentContext!).snackBarErrorTitle, 'No directory selected', SnackbarContentType.failure, ); diff --git a/app/lib/frontend/widgets/preference_selector.dart b/app/lib/frontend/widgets/preference_selector.dart index ebfa901..186393c 100644 --- a/app/lib/frontend/widgets/preference_selector.dart +++ b/app/lib/frontend/widgets/preference_selector.dart @@ -31,7 +31,7 @@ class PreferenceSelector extends StatefulWidget { }); @override - _PreferenceSelectorState createState() => _PreferenceSelectorState(); + State createState() => _PreferenceSelectorState(); } class _PreferenceSelectorState extends State { diff --git a/app/lib/main.dart b/app/lib/main.dart index cbbfec4..9f7bb80 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -29,11 +29,11 @@ void _preloadAssets() async { Future.wait( [ AssetManager.loadAsset('assets/graphics/animations/gpu.riv', - source: AssetSource.local), + source: AssetSource.local, type: AssetType.rivefile), AssetManager.loadAsset('assets/graphics/animations/human.riv', - source: AssetSource.local), + source: AssetSource.local, type: AssetType.rivefile), AssetManager.loadAsset('assets/metadata/ollama_models.json', - source: AssetSource.local), + source: AssetSource.local, type: AssetType.json), AssetManager.loadAsset('assets/graphics/logos/apache.svg', source: AssetSource.local), AssetManager.loadAsset('assets/graphics/logos/arduino.svg', diff --git a/app/pubspec.lock b/app/pubspec.lock index 654f36d..d77c70e 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -37,10 +37,34 @@ packages: dependency: transitive description: name: app_links - sha256: "96e677810b83707ff5e10fac11e4839daa0ea4e0123c35864c092699165eb3db" + sha256: f04c3ca96426baba784c736a201926bd4145524c36a1b38942a351b033305e21 url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.2.1" + app_links_linux: + dependency: transitive + description: + name: app_links_linux + sha256: f5f7173a78609f3dfd4c2ff2c95bd559ab43c80a87dc6a095921d96c05688c81 + url: "https://pub.dev" + source: hosted + version: "1.0.3" + app_links_platform_interface: + dependency: transitive + description: + name: app_links_platform_interface + sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + app_links_web: + dependency: transitive + description: + name: app_links_web + sha256: af060ed76183f9e2b87510a9480e56a5352b6c249778d07bd2c95fc35632a555 + url: "https://pub.dev" + source: hosted + version: "1.0.4" archive: dependency: "direct main" description: @@ -221,18 +245,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa" + sha256: dd09dd4e2b078992f42aac7f1a622f01882a8492fef08486b27ddde929c19f04 url: "https://pub.dev" source: hosted - version: "2.4.10" + version: "2.4.12" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799" + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "7.3.2" built_collection: dependency: transitive description: @@ -253,26 +277,26 @@ packages: dependency: "direct main" description: name: cached_network_image - sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.1" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" + sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" characters: dependency: transitive description: @@ -317,10 +341,10 @@ packages: dependency: transitive description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.0" convert: dependency: transitive description: @@ -333,18 +357,18 @@ packages: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" csslib: dependency: transitive description: @@ -389,18 +413,18 @@ packages: dependency: transitive description: name: device_info_plus - sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 url: "https://pub.dev" source: hosted - version: "10.1.0" + version: "10.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" dots_indicator: dependency: transitive description: @@ -469,10 +493,10 @@ packages: dependency: transitive description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" file: dependency: transitive description: @@ -485,10 +509,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" + sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "8.0.7" fixnum: dependency: transitive description: @@ -501,18 +525,18 @@ packages: dependency: "direct main" description: name: flex_color_picker - sha256: "31b27677d8d8400e4cff5edb3f189f606dd964d608779b6ae1b7ddad37ea48c6" + sha256: "809af4ec82ede3b140ed0219b97d548de99e47aa4b99b14a10f705a2dbbcba5e" url: "https://pub.dev" source: hosted - version: "3.5.0" + version: "3.5.1" flex_seed_scheme: dependency: transitive description: name: flex_seed_scheme - sha256: fb66cdb8ca89084e79efcad2bc2d9deb144666875116f08cdd8d9f8238c8b3ab + sha256: cc08c81879ecfd2ab840664ce4770980da0b8a319e35f51bcf763849b7f7596b url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "3.1.2" flutter: dependency: "direct main" description: flutter @@ -530,10 +554,10 @@ packages: dependency: transitive description: name: flutter_cache_manager - sha256: "395d6b7831f21f3b989ebedbb785545932adb9afe2622c1ffacf7f4b53a7e544" + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" url: "https://pub.dev" source: hosted - version: "3.3.2" + version: "3.4.1" flutter_highlight: dependency: "direct main" description: @@ -623,10 +647,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.0.21" flutter_shaders: dependency: transitive description: @@ -673,10 +697,10 @@ packages: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -689,10 +713,10 @@ packages: dependency: transitive description: name: functions_client - sha256: "48659e5c6a4bbe02659102bf6406a0cf39142202deae65aacfa78688f2e68946" + sha256: e63f49cd3b41727f47b3bde284a11a4ac62839e0604f64077d4257487510e484 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.2" gap: dependency: "direct main" description: @@ -721,10 +745,10 @@ packages: dependency: transitive description: name: google_identity_services_web - sha256: "9482364c9f8b7bd36902572ebc3a7c2b5c8ee57a9c93e6eb5099c1a9ec5265d8" + sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6" url: "https://pub.dev" source: hosted - version: "0.3.1+1" + version: "0.3.1+4" googleapis_auth: dependency: transitive description: @@ -737,10 +761,10 @@ packages: dependency: transitive description: name: gotrue - sha256: aaefc58b168723f8b5ca2a70ee8c0a051cba16f112be50f41c1ff8fb96b6a6df + sha256: "8703db795511f69194fe77125a0c838bbb6befc2f95717b6e40331784a8bdecb" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.8.4" gpu_info: dependency: "direct main" description: @@ -753,18 +777,18 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" grpc: dependency: "direct main" description: name: grpc - sha256: e93ee3bce45c134bf44e9728119102358c7cd69de7832d9a874e2e74eb8cab40 + sha256: "7b2bee509ea2750028fe15f5a530bdc8664e7e62e97abf0bb7b0fc65b6d7d2b3" url: "https://pub.dev" source: hosted - version: "3.2.4" + version: "4.0.0" gtk: dependency: transitive description: @@ -809,10 +833,10 @@ packages: dependency: "direct main" description: name: http - sha256: "761a297c042deedc1ffbb156d6e2af13886bb305c2a343a4d972504cd67dd938" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" http2: dependency: transitive description: @@ -833,10 +857,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4" url: "https://pub.dev" source: hosted - version: "4.0.2" + version: "4.1.0" image: dependency: "direct main" description: @@ -953,26 +977,26 @@ packages: dependency: "direct main" description: name: langchain - sha256: "64ffdf3f57dfbfad46115084ce7f628ae34ba76c024762400612dbbd7a458e0d" + sha256: "39688a1d66f89e83e209163bdd3595a46d37351adf0335de40756e2c40e6ebf4" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.4" langchain_core: dependency: transitive description: name: langchain_core - sha256: a1d84dba125b568b3a79646fd5b332a07092d80c83e18ccd8b2012b669769f3e + sha256: "62d53d0703a69499cffa0a393d834fb108ec75f16ee24f6c9fb1b2f31ed19ec0" url: "https://pub.dev" source: hosted - version: "0.3.3" + version: "0.3.4" langchain_ollama: dependency: "direct main" description: name: langchain_ollama - sha256: f9bdd5a905fb86f0b155ba50cd36484223821dbf969f35dc792b60dc1ee2b958 + sha256: b0850674f16d2d731197a3c2b13445f8ac3fce8230c204100c19a11bb9d989a3 url: "https://pub.dev" source: hosted - version: "0.2.2+1" + version: "0.3.0" langchain_tiktoken: dependency: transitive description: @@ -1025,10 +1049,10 @@ packages: dependency: "direct main" description: name: logger - sha256: af05cc8714f356fd1f3888fb6741cbe9fbe25cdb6eedbab80e1a6db21047d4a4 + sha256: "697d067c60c20999686a0add96cf6aba723b3aa1f83ecf806a8097231529ec32" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" logging: dependency: transitive description: @@ -1097,18 +1121,18 @@ packages: dependency: transitive description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" ollama_dart: dependency: transitive description: name: ollama_dart - sha256: "760cc0d1d26bb957b9fd3bb31985ebbfab5598076670185405469ec2600a5439" + sha256: c0a86adb2343543f5597d182dc64b32beb46e6f2692dda27f82f9294db07dae4 url: "https://pub.dev" source: hosted - version: "0.1.2" + version: "0.2.0" package_config: dependency: transitive description: @@ -1121,20 +1145,20 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: b93d8b4d624b4ea19b0a5a208b2d6eff06004bc3ce74c06040b120eeadd00ce0 + sha256: a75164ade98cb7d24cfd0a13c6408927c6b217fa60dee5a7ff5c116a58f28918 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "8.0.2" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" page_transition: - dependency: transitive + dependency: "direct main" description: name: page_transition sha256: dee976b1f23de9bbef5cd512fe567e9f6278caee11f5eaca9a2115c19dc49ef6 @@ -1161,18 +1185,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "9c96da072b421e98183f9ea7464898428e764bc0ce5567f27ec8693442e72514" + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -1201,10 +1225,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" petitparser: dependency: transitive description: @@ -1225,10 +1249,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -1249,10 +1273,10 @@ packages: dependency: transitive description: name: postgrest - sha256: f1f78470a74c611811132ff12acdef9c08b3ec65b61e88161a057d6cc5fbbd83 + sha256: c4197238601c7c3103b03a4bb77f2050b17d0064bf8b968309421abdebbb7f0e url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" protobuf: dependency: "direct main" description: @@ -1297,10 +1321,10 @@ packages: dependency: transitive description: name: realtime_client - sha256: cd44fa21407a2e217d674f1c1a33b36c49ad0d8aea0349bf5b66594db06c80fb + sha256: d897a65ee3b1b5ddc1cf606f0b83792262d38fd5679c2df7e38da29c977513da url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.1" recase: dependency: transitive description: @@ -1353,66 +1377,66 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" + sha256: a7e8467e9181cef109f601e3f65765685786c1a738a83d7fbbde377589c0d974 url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.1" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: name: shelf - sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.4.2" shelf_web_socket: dependency: transitive description: @@ -1470,10 +1494,10 @@ packages: dependency: transitive description: name: sqflite_common - sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + sha256: "7b41b6c3507854a159e24ae90a8e3e9cc01eb26a477c118d6dca065b5f55453e" url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.5.4+2" stack_trace: dependency: transitive description: @@ -1486,10 +1510,10 @@ packages: dependency: transitive description: name: storage_client - sha256: e37f1b9d40f43078d12bd2d1b6b08c2c16fbdbafc58b57bc44922da6ea3f5625 + sha256: "28c147c805304dbc2b762becd1fc26ee0cb621ace3732b9ae61ef979aab8b367" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" stream_channel: dependency: transitive description: @@ -1510,26 +1534,26 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" supabase: dependency: transitive description: name: supabase - sha256: "4555658031af0a8b38c7375f28e4b35312291f4aab0ca504dd76661381ce134f" + sha256: "4ed1cf3298f39865c05b2d8557f92eb131a9b9af70e32e218672a0afce01a6bc" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" supabase_flutter: dependency: "direct main" description: name: supabase_flutter - sha256: "1d6fb4ffaf50fc6f60507ab8ebf0b7dedbe6fabfbd8066db6f2a6552ddd0ea8c" + sha256: ff6ba3048fd47d831fdc0027d3efb99346d99b95becfcb406562454bd9b229c5 url: "https://pub.dev" source: hosted - version: "2.5.4" + version: "2.6.0" super_clipboard: dependency: transitive description: @@ -1558,10 +1582,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: a824e842b8a054f91a728b783c177c1e4731f6b124f9192468457a8913371255 url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.2.0" system_info2: dependency: "direct main" description: @@ -1574,10 +1598,10 @@ packages: dependency: "direct main" description: name: system_theme - sha256: "1f208db140a3d1e1eac2034b54920d95699c1534df576ced44b3312c5de3975f" + sha256: a32db6caa3a5129d02c03443121662959fba7ec1a8b01c78ee9a42718fbb3ef6 url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "3.0.0" system_theme_web: dependency: transitive description: @@ -1598,10 +1622,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.3" time: dependency: transitive description: @@ -1630,18 +1654,18 @@ packages: dependency: "direct main" description: name: unicons - sha256: dbfcf93ff4d4ea19b324113857e358e4882115ab85db04417a4ba1c72b17a670 + sha256: "1cca7462df18ff191b7e41b52f747d08854916531d1d7ab7cec0552095995206" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" units_converter: dependency: "direct main" description: name: units_converter - sha256: b4365ae8a672efa8e2dd4deec156f00b18829a654dedc1160a5273f0e97f6f12 + sha256: "90b96869527398a1b836cf5bc4f54432d5d9ded81026e817a01eaf3562d53309" url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" upower: dependency: transitive description: @@ -1654,34 +1678,34 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: "6ce1e04375be4eed30548f10a315826fd933c1e493206eab82eed01f438c8d2e" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.6" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: ceb2625f0c24ade6ef6778d1de0b2e44f2db71fded235eb52295247feba8c5cf + sha256: e35a698ac302dd68e41f73250bd9517fe3ab5fa4f18fe4647a0872db61bacbab url: "https://pub.dev" source: hosted - version: "6.3.3" + version: "6.3.10" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "7068716403343f6ba4969b4173cbf3b84fc768042124bc2c011e5d782b24fe89" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.3.0" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: dependency: transitive description: @@ -1702,26 +1726,26 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: "direct main" description: name: uuid - sha256: "814e9e88f21a176ae1359149021870e87f7cddaf633ab678a5d2b0bff7fd1ba8" + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" url: "https://pub.dev" source: hosted - version: "4.4.0" + version: "4.4.2" vector_graphics: dependency: transitive description: @@ -1755,7 +1779,7 @@ packages: source: hosted version: "2.1.4" visibility_detector: - dependency: transitive + dependency: "direct main" description: name: visibility_detector sha256: dd5cc11e13494f432d15939c3aa8ae76844c42b723398643ce9addb88a5ed420 @@ -1766,10 +1790,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" watcher: dependency: transitive description: @@ -1786,30 +1810,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.5" + version: "3.0.1" win32: dependency: transitive description: name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "5.5.4" win32_registry: dependency: transitive description: name: win32_registry - sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.4" windows_taskbar: dependency: "direct main" description: @@ -1854,10 +1886,10 @@ packages: dependency: transitive description: name: yet_another_json_isolate - sha256: e727502a2640d65b4b8a8a6cb48af9dd0cbe644ba4b3ee667c7f4afa0c1d6069 + sha256: "47ed3900e6b0e4dfe378811a4402e85b7fc126a7daa94f840fef65ea9c8e46f4" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.2" sdks: - dart: ">=3.4.3 <4.0.0" - flutter: ">=3.22.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 67af11d..50d745a 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -31,7 +31,7 @@ dependencies: # Theming and UI adaptive_theme: ^3.6.0 - system_theme: ^2.3.1 + system_theme: ^3.0.0 flex_color_picker: ^3.5.0 # Icons and Graphics @@ -96,16 +96,18 @@ dependencies: flutter_spinkit: ^5.2.1 # GRPC and Protocol Buffers - grpc: ^3.2.4 + grpc: ^4.0.0 protoc_plugin: ^21.1.2 protobuf: ^3.1.0 # LangChain Integration langchain: ^0.7.3 - langchain_ollama: ^0.2.2+1 + langchain_ollama: 0.3.0 introduction_screen: ^3.1.14 rive: ^0.13.12 + visibility_detector: any + page_transition: any dev_dependencies: flutter_test: sdk: flutter From 291245296e7ae4a3263d0aa88ab8ea1c0901d5ea Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Mon, 26 Aug 2024 12:53:08 +0200 Subject: [PATCH 12/13] Fix JSON output generation Boudled with some random changes to pubspec.yaml --- app/lib/backend/providers/chat.dart | 1 - app/pubspec.lock | 48 +++++++++++++++++------------ app/pubspec.yaml | 7 +++-- 3 files changed, 32 insertions(+), 24 deletions(-) diff --git a/app/lib/backend/providers/chat.dart b/app/lib/backend/providers/chat.dart index dc5554b..b971ba3 100644 --- a/app/lib/backend/providers/chat.dart +++ b/app/lib/backend/providers/chat.dart @@ -824,7 +824,6 @@ class ChatProvider extends ChangeNotifier { final modelOptions = ChatOllamaOptions( model: _modelName, numGpu: numGPU, - format: OllamaResponseFormat.json, keepAlive: _modelSettings.keepAlive ?? _keepAliveTime, temperature: _modelSettings.temperature ?? _temperature, concurrencyLimit: _modelSettings.concurrencyLimit ?? 1000, diff --git a/app/pubspec.lock b/app/pubspec.lock index d77c70e..9892dcf 100644 --- a/app/pubspec.lock +++ b/app/pubspec.lock @@ -341,10 +341,10 @@ packages: dependency: transitive description: name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a url: "https://pub.dev" source: hosted - version: "1.19.0" + version: "1.18.0" convert: dependency: transitive description: @@ -857,10 +857,10 @@ packages: dependency: transitive description: name: http_parser - sha256: "40f592dd352890c3b60fec1b68e786cefb9603e05ff303dbc4dda49b304ecdf4" + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.0.2" image: dependency: "direct main" description: @@ -977,26 +977,26 @@ packages: dependency: "direct main" description: name: langchain - sha256: "39688a1d66f89e83e209163bdd3595a46d37351adf0335de40756e2c40e6ebf4" + sha256: "331f4ba4ce6acd26a52fd88df590dd303ec0fcf62c4b406653aaed69a6481a0a" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.5" langchain_core: dependency: transitive description: name: langchain_core - sha256: "62d53d0703a69499cffa0a393d834fb108ec75f16ee24f6c9fb1b2f31ed19ec0" + sha256: "59c9914f9f542d728709d631923cfad7d6d09b7a9dce0f9cec7b508034ff27fb" url: "https://pub.dev" source: hosted - version: "0.3.4" + version: "0.3.5" langchain_ollama: dependency: "direct main" description: name: langchain_ollama - sha256: b0850674f16d2d731197a3c2b13445f8ac3fce8230c204100c19a11bb9d989a3 + sha256: "60df0ab438076cccd8c4ca4993691d65bb6ca42186a999d009744534cfa8af67" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.1" langchain_tiktoken: dependency: transitive description: @@ -1129,10 +1129,10 @@ packages: dependency: transitive description: name: ollama_dart - sha256: c0a86adb2343543f5597d182dc64b32beb46e6f2692dda27f82f9294db07dae4 + sha256: f65e60c61c91f625df76b4435474bfacc315b4b4eaa8b53e2385679da121dee4 url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" package_config: dependency: transitive description: @@ -1433,10 +1433,10 @@ packages: dependency: transitive description: name: shelf - sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12 + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.2" + version: "1.4.1" shelf_web_socket: dependency: transitive description: @@ -1534,10 +1534,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.2.0" supabase: dependency: transitive description: @@ -1622,10 +1622,10 @@ packages: dependency: transitive description: name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.3" + version: "0.7.2" time: dependency: transitive description: @@ -1634,6 +1634,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + timelines_plus: + dependency: "direct main" + description: + name: timelines_plus + sha256: ad0d97a0ee21942ee749c603b117794eb97cc6788f0cadc7d76cf1bbb2c26fca + url: "https://pub.dev" + source: hosted + version: "1.0.3" timing: dependency: transitive description: @@ -1790,10 +1798,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.2.4" watcher: dependency: transitive description: diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 50d745a..4704b15 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -33,6 +33,9 @@ dependencies: adaptive_theme: ^3.6.0 system_theme: ^3.0.0 flex_color_picker: ^3.5.0 + visibility_detector: any + page_transition: any + timelines_plus: ^1.0.3 # Icons and Graphics unicons: ^2.1.1 @@ -102,12 +105,10 @@ dependencies: # LangChain Integration langchain: ^0.7.3 - langchain_ollama: 0.3.0 + langchain_ollama: ^0.3.1 introduction_screen: ^3.1.14 rive: ^0.13.12 - visibility_detector: any - page_transition: any dev_dependencies: flutter_test: sdk: flutter From 79340a1fc6c5e095e8d80f372cf9860b2b4d665b Mon Sep 17 00:00:00 2001 From: Wilielmus <88447902+WilliamKarolDiCioccio@users.noreply.github.com> Date: Mon, 26 Aug 2024 14:28:45 +0200 Subject: [PATCH 13/13] Refactor chat provider to use message streaming --- app/lib/backend/providers/chat.dart | 133 ++++++++++++---------------- 1 file changed, 57 insertions(+), 76 deletions(-) diff --git a/app/lib/backend/providers/chat.dart b/app/lib/backend/providers/chat.dart index b971ba3..a3a7316 100644 --- a/app/lib/backend/providers/chat.dart +++ b/app/lib/backend/providers/chat.dart @@ -308,24 +308,41 @@ class ChatProvider extends ChangeNotifier { /// If the session is not selected, the function returns the newly created [ChatModelMessageWrapper] without adding it to the memory or the database. /// /// Returns the newly created [ChatModelMessageWrapper]. - ChatModelMessageWrapper addModelMessage(String message, String senderName) { + StreamSubscription addModelMessage( + Stream messageStream, + String senderName, + ) { + final StringBuffer messageBuffer = StringBuffer(); + final DateTime timestamp = DateTime.now(); + final String messageId = const Uuid().v4(); + final chatMessage = ChatModelMessageWrapper( - message, - DateTime.now(), - const Uuid().v4(), + '', + timestamp, + messageId, senderName, ); - if (!isSessionSelected) return chatMessage; - _session!.messages.add(chatMessage); - _session!.memory.chatHistory.addAIChatMessage(message); - ChatSessionsDatabase.updateSession(_session!); + final StreamSubscription subscription = messageStream.listen( + (message) { + messageBuffer.write(message); - notifyListeners(); + _session!.messages.last.text = messageBuffer.toString(); + }, + onDone: () { + if (isSessionSelected) { + _session!.memory.chatHistory.addAIChatMessage( + messageBuffer.toString(), + ); + ChatSessionsDatabase.updateSession(_session!); + notifyListeners(); + } + }, + ); - return _session!.messages.last as ChatModelMessageWrapper; + return subscription; } /// Adds a chat message of type user to the current session and to the model's memory and updates the session in the database. @@ -465,6 +482,34 @@ class ChatProvider extends ChangeNotifier { return prompt; } + /// Processes the chat chain with the given prompt and streams the response as a string. + Stream _processChain(ChatMessage prompt) async* { + final chain = await _buildChain(); + + await for (final response in chain.stream([prompt])) { + final result = response as ChatResult; + + _computePerformanceStatistics(result); + + yield response.outputAsString; + + notifyListeners(); + + // If the session is aborted, remove the last message from memory and break the loop + + if (_session!.status == ChatSessionStatus.aborting) { + _session!.status = ChatSessionStatus.idle; + _session!.memory.chatHistory.removeLast(); + + _computePerformanceStatistics(result); + + notifyListeners(); + + break; + } + } + } + /// Sends a message to the chat model and processes the response. /// /// The function first checks if a session is selected and creates a new one if not. @@ -504,43 +549,9 @@ class ChatProvider extends ChangeNotifier { addUserMessage(text, imageBytes); - final chain = await _buildChain(); - final prompt = _buildPrompt(text, imageBytes: imageBytes); - addModelMessage('', _modelName); - - await for (final response in chain.stream([prompt])) { - ChatResult result = response as ChatResult; - - // If the session is aborted, remove the last message from memory and break the loop - - if (_session!.status == ChatSessionStatus.aborting) { - _session!.status = ChatSessionStatus.idle; - _session!.memory.chatHistory.removeLast(); - - _computePerformanceStatistics(result); - - notifyListeners(); - - break; - } - - final lastMessage = _session!.messages.last; - lastMessage.text += result.outputAsString; - - _computePerformanceStatistics(result); - - notifyListeners(); - } - - // Save the generated message, remove and add it back to force a memory update - - final generatedText = _session!.messages.last.text; - - removeLastMessage(); - - addModelMessage(generatedText, _modelName); + addModelMessage(_processChain(prompt), _modelName); _session!.status = ChatSessionStatus.idle; @@ -638,42 +649,12 @@ class ChatProvider extends ChangeNotifier { notifyListeners(); - final chain = await _buildChain(); - final prompt = _buildPrompt( userMessage.text, imageBytes: userMessage.imageBytes, ); - addModelMessage('', _modelName); - - await for (final response in chain.stream([prompt])) { - ChatResult result = response as ChatResult; - - if (_session!.status == ChatSessionStatus.aborting) { - _session!.status = ChatSessionStatus.idle; - _session!.memory.chatHistory.removeLast(); - - _computePerformanceStatistics(result); - - notifyListeners(); - - break; - } - - final lastMessage = _session!.messages.last; - lastMessage.text += result.outputAsString; - - _computePerformanceStatistics(result); - - notifyListeners(); - } - - final generatedText = _session!.messages.last.text; - - removeLastMessage(); - - addModelMessage(generatedText, _modelName); + addModelMessage(_processChain(prompt), _modelName); _session!.status = ChatSessionStatus.idle;