QOL: Mzansi AI Enhancement pt1

This commit is contained in:
2025-11-27 09:48:42 +02:00
parent 08bfe1d417
commit cc3f18f7e2
10 changed files with 231 additions and 176 deletions

View File

@@ -98,131 +98,166 @@ class _MihDropdownFieldState extends State<MihDropdownField> {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Theme(
data: Theme.of(context).copyWith(
textSelectionTheme: TextSelectionThemeData(
cursorColor: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
selectionColor: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark")
.withValues(alpha: 0.3),
selectionHandleColor: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
Row(
children: [
Expanded(
child: Theme(
data: Theme.of(context).copyWith(
textSelectionTheme: TextSelectionThemeData(
cursorColor: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
selectionColor: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!
.theme
.mode ==
"Dark")
.withValues(alpha: 0.3),
selectionHandleColor: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
),
),
child: DropdownMenu(
controller: widget.controller,
dropdownMenuEntries: menu,
enableSearch: widget.enableSearch,
enableFilter: widget.enableSearch,
enabled: widget.editable,
textInputAction: widget.enableSearch
? TextInputAction.search
: TextInputAction.none,
requestFocusOnTap: widget.enableSearch,
menuHeight: 400,
expandedInsets: EdgeInsets.zero,
textStyle: TextStyle(
color: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
fontWeight: FontWeight.w500,
),
trailingIcon: Icon(
Icons.arrow_drop_down,
color: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
),
selectedTrailingIcon: Icon(
Icons.arrow_drop_up,
color: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
),
// leadingIcon:
// IconButton(
// onPressed: () {
// widget.controller.clear();
// field.didChange('');
// },
// icon: Icon(
// Icons.delete_outline_rounded,
// color: MihColors.getPrimaryColor(
// MzansiInnovationHub.of(context)!.theme.mode ==
// "Dark"),
// ),
// ),
onSelected: (String? selectedValue) {
field.didChange(selectedValue);
widget.onSelected?.call(selectedValue);
},
menuStyle: MenuStyle(
backgroundColor: WidgetStatePropertyAll(
MihColors.getSecondaryColor(
MzansiInnovationHub.of(context)!
.theme
.mode ==
"Dark")),
side: WidgetStatePropertyAll(
BorderSide(
color: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!
.theme
.mode ==
"Dark"),
width: 1.0),
),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
10), // Increase for more roundness
),
),
),
inputDecorationTheme: InputDecorationTheme(
errorStyle: const TextStyle(height: 0, fontSize: 0),
contentPadding: const EdgeInsets.symmetric(
horizontal: 10.0, vertical: 8.0),
filled: true,
fillColor: MihColors.getSecondaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(
color: field.hasError
? MihColors.getRedColor(
MzansiInnovationHub.of(context)!
.theme
.mode ==
"Dark")
: MihColors.getSecondaryColor(
MzansiInnovationHub.of(context)!
.theme
.mode ==
"Dark"),
width: 3.0,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(
color: MihColors.getRedColor(
MzansiInnovationHub.of(context)!
.theme
.mode ==
"Dark"),
width: 3.0,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(
color: MihColors.getRedColor(
MzansiInnovationHub.of(context)!
.theme
.mode ==
"Dark"),
width: 3.0,
),
),
),
),
),
),
),
child: DropdownMenu(
controller: widget.controller,
dropdownMenuEntries: menu,
enableSearch: widget.enableSearch,
enableFilter: widget.enableSearch,
enabled: widget.editable,
textInputAction: widget.enableSearch
? TextInputAction.search
: TextInputAction.none,
requestFocusOnTap: widget.enableSearch,
menuHeight: 400,
expandedInsets: EdgeInsets.zero,
textStyle: TextStyle(
color: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
fontWeight: FontWeight.w500,
),
trailingIcon: Icon(
Icons.arrow_drop_down,
color: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
),
selectedTrailingIcon: Icon(
Icons.arrow_drop_up,
color: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
),
leadingIcon: IconButton(
onPressed: () {
const SizedBox(width: 8),
GestureDetector(
onTap: () {
widget.controller.clear();
field.didChange('');
},
icon: Icon(
Icons.delete_outline_rounded,
color: MihColors.getPrimaryColor(
child: Icon(
size: 35,
Icons.delete_rounded,
color: MihColors.getSecondaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
),
),
onSelected: (String? selectedValue) {
field.didChange(selectedValue);
widget.onSelected?.call(selectedValue);
},
menuStyle: MenuStyle(
backgroundColor: WidgetStatePropertyAll(
MihColors.getSecondaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark")),
side: WidgetStatePropertyAll(
BorderSide(
color: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
width: 1.0),
),
shape: WidgetStatePropertyAll(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
10), // Increase for more roundness
),
),
),
inputDecorationTheme: InputDecorationTheme(
errorStyle: const TextStyle(height: 0, fontSize: 0),
contentPadding: const EdgeInsets.symmetric(
horizontal: 10.0, vertical: 8.0),
filled: true,
fillColor: MihColors.getSecondaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(
color: field.hasError
? MihColors.getRedColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark")
: MihColors.getSecondaryColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
width: 3.0,
),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(
color: MihColors.getRedColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
width: 3.0,
),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8.0),
borderSide: BorderSide(
color: MihColors.getRedColor(
MzansiInnovationHub.of(context)!.theme.mode ==
"Dark"),
width: 3.0,
),
),
),
),
],
),
if (field.hasError)
Padding(

View File

@@ -228,10 +228,11 @@ class _MihAiChatState extends State<MihAiChat> with WidgetsBindingObserver {
LlmChatView(
provider: aiProvider.ollamaProvider,
messageSender: aiProvider.ollamaProvider.sendMessageStream,
speechToText: aiProvider.ollamaProvider.speechToText,
// welcomeMessage:
// "Mzansi AI is here to help. Send us a messahe and we'll try our best to assist you.",
autofocus: false,
enableAttachments: false,
enableAttachments: true,
enableVoiceNotes: false,
style: aiProvider.getChatStyle(context),
suggestions: [

View File

@@ -19,7 +19,10 @@ class MzansiAiProvider extends ChangeNotifier {
}) {
ollamaProvider = OllamaProvider(
baseUrl: "${AppEnviroment.baseAiUrl}/api",
model: AppEnviroment.getEnv() == "Prod" ? 'gemma3n:e4b' : "gemma3:1b",
model: AppEnviroment.getEnv() == "Prod"
? 'gemma3n:e4b'
: "qwen3-vl:2b-instruct",
think: false,
systemPrompt: "You are Mzansi AI, a helpful and friendly AI assistant running on the 'MIH App'.\n" +
"The MIH App was created by 'Mzansi Innovation Hub', a South African-based startup company." +
"Your primary purpose is to assist users by answering general questions and helping with creative writing tasks or any other task a user might have for you.\n" +
@@ -147,9 +150,6 @@ class MzansiAiProvider extends ChangeNotifier {
MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
progressIndicatorColor: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
menuColor: Colors.black,
// MihColors.getGreenColor(
// MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
disabledButtonStyle: ActionButtonStyle(
icon: MihIcons.mzansiAi,
iconColor: MihColors.getSecondaryColor(
@@ -323,6 +323,21 @@ class MzansiAiProvider extends ChangeNotifier {
MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
),
),
addButtonStyle: ActionButtonStyle(
iconDecoration: BoxDecoration(
color: MihColors.getSecondaryColor(
MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
borderRadius: BorderRadius.circular(25),
),
iconColor: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
textStyle: TextStyle(
color: MihColors.getPrimaryColor(
MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
),
),
menuColor: MihColors.getSecondaryColor(
MzansiInnovationHub.of(context)!.theme.mode == "Dark"),
);
}
}

View File

@@ -4,14 +4,16 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_ai_toolkit/flutter_ai_toolkit.dart';
import 'package:ken_logger/ken_logger.dart';
import 'package:ollama_dart/ollama_dart.dart';
import 'package:cross_file/cross_file.dart';
class OllamaProvider extends LlmProvider with ChangeNotifier {
OllamaProvider({
String? baseUrl,
Map<String, String>? headers,
Map<String, String>? queryParams,
Map<String, dynamic>? queryParams,
required String model,
String? systemPrompt,
bool? think,
}) : _client = OllamaClient(
baseUrl: baseUrl,
headers: headers,
@@ -19,11 +21,13 @@ class OllamaProvider extends LlmProvider with ChangeNotifier {
),
_model = model,
_systemPrompt = systemPrompt,
_think = think,
_history = [];
final OllamaClient _client;
final String _model;
final List<ChatMessage> _history;
final String? _systemPrompt;
final bool? _think;
@override
Stream<String> generateStream(
@@ -36,6 +40,29 @@ class OllamaProvider extends LlmProvider with ChangeNotifier {
yield* _generateStream(messages);
}
Stream<String> speechToText(XFile audioFile) async* {
KenLogger.success("Inside Custom speechToText funtion");
// 1. Convert the XFile to the attachment format needed for the LLM.
final attachments = [await FileAttachment.fromFile(audioFile)];
KenLogger.success("added attachment for audio file");
// 2. Define the transcription prompt, mirroring the logic from LlmChatView.
const prompt =
'translate the attached audio to text; provide the result of that '
'translation as just the text of the translation itself. be careful to '
'separate the background audio from the foreground audio and only '
'provide the result of translating the foreground audio.';
KenLogger.success("Created Prompt");
// 3. Use your existing Ollama API call to process the prompt and attachment.
// We are essentially running a new, one-off chat session for transcription.
yield* generateStream(
prompt,
attachments: attachments,
);
KenLogger.success("done");
}
@override
Stream<String> sendMessageStream(
String prompt, {
@@ -76,6 +103,7 @@ class OllamaProvider extends LlmProvider with ChangeNotifier {
Stream<String> _generateStream(List<Message> messages) async* {
final allMessages = <Message>[];
if (_systemPrompt != null && _systemPrompt.isNotEmpty) {
KenLogger.success("Adding system prompt to the conversation");
allMessages.add(Message(
role: MessageRole.system,
content: _systemPrompt,
@@ -87,6 +115,7 @@ class OllamaProvider extends LlmProvider with ChangeNotifier {
request: GenerateChatCompletionRequest(
model: _model,
messages: allMessages,
think: _think,
),
);
// final stream = _client.generateChatCompletionStream(
@@ -109,19 +138,32 @@ class OllamaProvider extends LlmProvider with ChangeNotifier {
content: message.text ?? '',
);
}
final imageAttachments = <String>[];
final docAttachments = <String>[];
if (message.text != null && message.text!.isNotEmpty) {
docAttachments.add(message.text!);
}
for (final attachment in message.attachments) {
if (attachment is FileAttachment) {
final mimeType = attachment.mimeType.toLowerCase();
if (mimeType.startsWith('image/')) {
imageAttachments.add(base64Encode(attachment.bytes));
} else if (mimeType == 'application/pdf' ||
mimeType.startsWith('text/')) {
throw LlmFailureException(
"\n\nAww, that file is a little too advanced for us right now ($mimeType)! We're still learning, but we'll get there! Please try sending us a different file type.\n\nHint: We can handle images quite well!",
);
}
} else {
throw LlmFailureException(
'Unsupported attachment type: $attachment',
);
}
}
return Message(
role: MessageRole.user,
content: message.text ?? '',
images: [
for (final attachment in message.attachments)
if (attachment is ImageFileAttachment)
base64Encode(attachment.bytes)
else
throw LlmFailureException(
'Unsupported attachment type: $attachment',
),
],
content: docAttachments.join(' '),
images: imageAttachments,
);
case MessageOrigin.llm: